지난 5년간 일어났던 전세계 프로그래밍 트렌드의 변화 중 가장 큰 것을 뽑으라면 단연 함수형 프로그래밍의 대중화를 들 수 있겠다. 새로 나오는 거의 모든 언어가 함수형 프로그래밍을 지원하고 있고, 함수형 스타일로 만든 프로그램, 프레임워크, 라이브러리들이 넘쳐날 정도로 많아졌다. 심지어 어떤 언어로 프로그래밍을 하는 개발자들이 전세계적으로 연봉을 제일 많이 받나 설문해 보니 함수형 언어 개발자들이 최상위권을 차지하고 있다는 보고도 있다:
1위부터 5위까지 중 4개가 함수형 프로그래밍 언어다.
그런데 이상하게도 이런 함수형 프로그래밍의 전세계적인 유행 속에 우리나라만 쏙 빠져 있는 느낌이다. 당장 구글, 트위터, 페이스북, GitHub 등에 함수형 프로그래밍을 검색해 보면 글 자체가 그렇게 많지도 않지만 2018년을 기점으로는 아예 새로 올라오는 글 자체가 손으로 꼽을 수 있을 만큼 적은 것 같다. 우리나라도 IT로 먹고 사는 나라에다 프로그래머의 수가 몇만명이나 되는데…
그래서 이 글을 시작으로 당분간 함수형 프로그래밍에 관한 글을 시리즈로 써보려고 한다. 오늘은 함수형 프로그래밍이 무엇이고 왜 필요한지에 관해 짤막히 소개하도록 하겠다.
따로 언급하지 않으면 모든 코드는 F#으로 작성한 것이다. F#은 마이크로소프트에서 만든 오픈소스 함수형 프로그래밍 언어다. Visual Studio에도 이미 몇년전부터 공식 언어로 포함되어 있어서 누구나 무료로 쉽게 받아 쓸 수 있다.
함수형 프로그래밍이란 무엇인가
위키피디어에서 찾아 보면 아래와 같이 나온다:
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
소위 객체지향 프로그래밍이 오브젝트간의 복잡한 상호작용을 다각도로 기술하는 방식인 반면, 함수형 프로그래밍은 마치 공장 컨베이어 생산 라인처럼 입력을 함수에 넣고 여기서 얻은 리턴값을 다른 함수에 입력으로 넣고 다시 여기서 받은 리턴값을 다른 함수에 넣고…를 반복하면서 데이터를 가공하여 최종 결과를 얻는 방식이다. 이 과정에서 함수형 프로그래밍을 비함수형 프로그래밍과 구분짓는 가장 중요한 요소가 바로 불변성(immutability)이 되겠다(위키피디어 설명에 나와 있는 “avoids changing-state and mutable data”란 부분). 불변성은 변수의 값을 한번 정해 놓으면 영원히 바꿀 수 없는 성질을 뜻한다(값을 바꿀 수 없는 변수라니 어폐가 있긴 하다).
예를 들어 C/C++/C#/자바 같은 비함수형 프로그래밍 언어에서는 다음과 같은 코드가 매우 일반적이다:
int i = 100;
i++;
i = i + 2;
i += 3;
반면 F#에서는 아래와 같이 하면 컴파일 에러가 난다:
let i = 100
i = i + 1 // 컴파일 에러
변수의 값을 바꾸고 싶다면 최초 정의할 때 mutable
이란 키워드를 붙여 주어야 한다:
let mutable i = 100
i <- i + 1
F#에서는 C 계열 언어에서 흔히 쓰는 =
를 대입 연산자로 쓰지 못한다. =
는 수학적 의미 그대로 좌변과 우변이 같다는 뜻으로만 쓰이고, 대신 <-
를 대입 연산자로 쓴다. 따라서 F# 코드에서 mutable
이나 <-
등이 보인다면 이 부분은 함수형 스타일로 짠 것이 아님을 금방 알 수 있다. 실제로 이들은 제한적인 용도로만 아주 가끔 쓰인다.
변수의 값을 바꾸지 않고 프로그래밍이 어떻게 가능할까? 의외로 간단하다. 값을 바꿀 때마다 변수를 새로 만들면 된다:
let i = 100
let j = i + 1
let k = j + 2
let l = k + 3
말이 안된다고 생각하는 분이 있을지 모르지만 개념상으로는 정말로 이렇다. 이 간단한 아이디어가 함수형 프로그래밍의 근본이자 핵심으로, 나중 다양한 분야에서 놀라운 위력을 발휘하게 된다.
그런데 실제로는 저렇게 변수를 끝없이 새로 만들어서 쓰는 게 아니라 다른 방법으로 한다. 바로 재귀 함수를 쓰는 것이다. 재귀 함수는 자기 자신을 호출하는 함수를 뜻하는데, 함수 내에서 어떤 값을 바꿀 필요가 있다면 그 바꾼 값을 자기 자신에게 인수로 넘겨 주면서 함수를 처음부터 다시 시작하는 방식이다. 예를 들어 다음과 같이 한다:
// rec은 재귀 함수임을 컴파일러에게 알려주는 키워드
let rec func n j =
...
func (n + 1) (j - 1)
이렇게 재귀 호출을 할 때마다 매번 n
의 값은 1
씩 증가하고 j
의 값은 1
씩 감소할 것이다. 그런데 이 코드는 사실 아래 코드의 축약판이다:
let rec func n j =
...
let n' = n + 1
let j' = j - 1
func n' j'
값을 바꿀 때마다 변수를 새로 만들면 된다는 것이 정말로 가능함을 알 수 있다(!).
그런데 아이러니하게도 함수형 프로그래밍이 인기가 없었던 이유 중 하나가 바로 이 재귀 함수 때문이다. 재귀 함수를 이해하고 만들기가 실제로는 엄청나게 어렵기 때문이다. 예전에 C 언어를 처음 배울 때 모든 책마다 재귀 함수에 관한 설명이 빠짐없이 있었던 걸로 기억이 되는데, 왜 이런 이상한 방법을 쓰는지 F#을 배우기 전까지 무려 20년이 넘게 이해를 못했었다. 정답은 함수형 프로그래밍을 하기 위해서. 반대로 함수형 프로그래밍이 아니라면 굳이 재귀 함수를 써야 할 이유가 거의 없기 때문에 왜 쓰는지 이해가 가지 않는 것도 어쩌면 당연한 일이었다. 재귀 함수를 이해하고 쉽게 만드는 방법에 관해서는 조만간 따로 다루도록 하겠다.
함수형 프로그래밍을 해야 하는 이유
함수형 프로그래밍의 장점은 명확하다:
(훨씬) 더 짧고 (훨씬) 더 이해하기 쉽고 (훨씬) 더 버그가 적은 코드를 (훨씬) 더 짧은 시간 안에 만들 수 있다.
(훨씬) 더 자세한 이야기는 다음 글에 계속.