요즘 프로그래머들 사이에서 가장 뜨거운 이슈 하나를 꼽자면, 난 함수형 프로그래밍(Functional Programming)을 꼽을 것이다. 특히 요새 들어 함수형 프로그래밍 열풍이라는 것을 많이 느낀다. Java, C++, C#, JavaScript 등 사용자 층이 두텁고 각각의 철학이 있는 언어에서 함수형 패러다임을 앞다투어 차용하고 있는 현상은 물론이고, 미국에서는 스칼라가 많이 쓰인다 느니, 해외 취업을 위해선 스칼라를 공부해야 된다는 이야기부터, 회사 동료들 책상 위에 스칼라(Scala)나 하스켈(Haskell) 등의 함수형 언어 책이 놓여있는 것이 종종 보이는 것까지. 확실히 함수형 프로그래밍이 프로그래밍 패러다임에 ‘대세’를 이끌고 있다. 더군다나 얼마 전에는 사내 교육으로 임백준 님의 Reactive Programming과 Kevin Lee님의 Functional Programming 주제의 세미나를 들을 기회도 있었다. 이를 계기로 함수형 프로그래밍에 대해 더욱 많은 흥미가 생겼는데, 도대체 그 대세 함수형 프로그래밍이 무엇인지 정리해보려고 한다.
함수형 프로그래밍은 순수 함수를 이용한다
함수형 프로그래밍이란 무엇일까? 위키피디아에서는 ‘자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나’라고 정의하고 있다. 쉽게 이해되지는 않는 말이었는데, 얼마 전 세미나에서 Kevin님께서는 ‘동일한 입력에, 동일한 결과를 얻는 것’이라고 간단한 말로 설명해주셨다. 즉, 함수형 프로그래밍은 외부 영향을 받는 입력 및 출력을 최대한 제거한 함수(순수 함수)를 이용한 프로그래밍을 말한다. 위키피디아에 있는 예로 ‘순수 함수’에 대해 부가적인 설명을 하자면, f가 순수 함수라고 할 때, 아래 코드에서 sampleA 와 sampleB의 결과는 항상 같다.
1
2
3
4
5
6
7
int sampleA(int x) {
return f(x) * f(x);
}
int sampleB(int x) {
int a = f(x);
return a * a;
}
그러나 f가 순수하지 않은 함수의 경우, 결과가 같지 않을 수가 있다. 아래 코드의 sampleC 함수와 sampleD 함수가 그 예이다. (함수 random은 임의의 값을 반환하는 함수라고 하자)
1
2
3
4
5
6
7
int sampleC(int x) {
return random() * random();
}
int sampleD(int x) {
int a = random();
return a * a;
}
마찬가지로, C언어에서 단순히 글자를 출력하는 함수 printf 역시, 순수 함수가 아니다. C의 함수 printf는 정상적으로 출력이 되었는지 여부에 따라 0과 1을 반환하기 때문이다. 함수형 프로그래밍에 대해 다시 한번 정의하면, 함수형 프로그래밍은 부작용(side-effect)없이 외부의 영향을 받지 않는 안전한(thread-safe) 함수를 사용하는 프로그래밍이다.
그럼 반대로 ‘부작용이 있는’ 함수는 무슨 문제가 있는 것일까? 함수형 프로그래밍 개발자로 유명한 Kris Jenkins가 쓴 블로그 “What is Functional Programming”에서는 부작용을 ‘복잡성 빙산’ 이라고 표현한다. 아래 설명에서는 블로그에서 든 예시와 내용을 빌리겠다. (정확히 말하면 이를 번역한 한주영 님이 쓴 블로그 ‘함수형 프로그래밍이란 무엇인가?’ 의 내용을 빌리겠다)
1
public boolean processMessage(Channel channel) {...}
위의 코드를 보고 위 함수의 기능을 알 수 있다고 생각하지만, 그 함수의 표면 아래에 숨겨진 것은 무엇이든 될 수 있기 때문에, 그 구현을 보지 않고 기능을 알 수 없다. 즉, 구현을 보지 않고서는 정말 어떤 것들이 연관되어 있을지 전혀 알 수 없기 때문에, 잠재적으로 엄청나게 큰 복잡성이 숨어 있다는 것이다 (‘캡슐화’의 개념에 대해 이야기하는 것이 아닌, ‘코드와 외부 세상과의 관계를 숨기는 것’에 대한 개념에 대해 말하는 것이다). 부작용이 있는 함수가 원래 작성한 프로그래머 예상대로 정확히 동작하면 괜찮다고 생각할 수 있겠지만, 함수가 작성되었을 때 기대했던 것과 똑같은 환경일지, 혹시 누가 어딘가를 바꾸지 않았을지, 겉으로 봐서는 전혀 연관이 없어 보이는 코드 조각을 수정했을 지 모른다. 그러나 함수형 프로그래밍은 부작용을 표면으로 드러낼 수 있다. 아래의 예는 숨겨진 입력 (new Date()) 대신, 파라미터를 추가로 입력 (Date when) 받도록 바꾸어 순수 함수로 구현한 것이다.
1
2
3
4
5
public Program getCurrentProgram(TVGuide guide, int channel) {
Schedule schedule = guide.getSchedule(channel);
Program current = schedule.programAt(new Date()); // 숨겨진 입력
return current;
}
1
2
3
4
5
public Program getProgramAt(TVGuide guide, int channel, Date when) {
Schedule schedule = guide.getSchedule(channel);
Program program = schedule.programAt(when); // 파라미터로 전환
return program;
}
이로써, 두개가 아닌 세개의 인자를 가져 복잡해 보일 수도 있지만, 실제로는 의존성을 정직하게 드러내 더 복잡하지 않고, 테스트도 쉽고, 추론하기도 쉬워졌다. 함수형 프로그래밍은 모든 언어에서 가능하고, 함수형 프로그래밍 언어는 부작용 없는 프로그래밍을 지원하고 장려하는 언어이다.
함수형 프로그래밍은 간결하다.
어떤 개체가 있을 때, 아래의 조건을 만족하면 일급 시민이라고 부른다. (1960년대 영국 컴퓨터 과학자가 만들어 낸 단어라고 한다)
1. 변수나 데이터 구조 안에 담을 수 있다.
2. 파라미터로 전달 할 수 있다.
3. 반환 값으로 사용할 수 있다.
함수형 프로그래밍은 함수 자체가 위 3개의 조건을 모두 만족한다. 즉, 함수형 프로그래밍 언어는 함수 자체가 객체 지향 언어의 클래스와 같은 일종의 타입으로 동작한다. 함수를 변수처럼 다룰 수도 있고, 다른 함수에 파라미터로 바로 넘겨줄 수도 있고, 함수를 만드는 함수(함수를 반환 값으로 가지는 함수)를 정의할 수도 있다. 이러한 특징으로 함수형 프로그래밍은 코드가 매우 간결하다.
Java의 경우에도 1.8 버전부터는 method reference 기능을 제공하여 메서드(함수) 자체가 일급 시민이 되었다. 아래는 Java 1.8 버전에서 method reference 를 사용한 예이다.
1
2
3
4
5
6
7
public static void main(String[] args) {
print(String::toLowerCase, "STRING TO LOWERCASE");
print(String::toUpperCase, "string to uppercase");
}
public static void print(Function<String, String> function, Strings) {
System.out.println(function.apply(s));
}
또한, 함수형 프로그래밍은 대입문(assignment statements)없이 코딩하기 때문에 코드가 짧고 읽기 쉽다. 아래는 1부터 10까지의 숫자를 제곱한 값을 출력하는 Java코드이다.
1
2
3
4
5
public static void main(String args[]) {
for (int i=1; i<=25; i++) {
System.out.println(i*i);
}
}
이를 클로저(Closure) 언어로 구현하면 아래와 같다.
(take 25 (squares-of (integers)))
함수형 프로그래밍은 동시성(concurrency) 프로그래밍이 쉽게 가능하다.
명령형 프로그래밍의 경우 동시성 프로그래밍을 설계하기도 어렵고 사용하기도 어렵다. 뮤텍스 관리를 따로 해야 하기 때문에, 가장 느린 스레드의 속도에 다른 모든 스레드가 맞춰야 하는 등 고려해야할 사항이 많기 때문이다. 반면에, 함수형 프로그래밍은 영속적 자료구조(persistent data structure)이다. 즉, 개체들이 immutable한 속성이기때문에 스레드 안정성을 보장한다. 또한 공유할 상태가 없으므로, 병목 지점이 사라지고 모든 코어, 모든 컴퓨터가 각자 낼 수 있는 가장 빠른 속도로 동작하게 된다. 이러한 장점 덕분에 함수형 프로그래밍은 비동기(Asynchronous)적으로 처리해야 하는 프로그래밍에 많이 사용된다. 비동기 프로그래밍은 동기(Synchronous) 프로그래밍과 구분이 되는데, 다음 동작 전에 필요한 값을 기다리지 않고 바로 처리하는 것을 말한다. (동기/비동기와 헷갈리는 개념으로 Blocking/Non-Blocking이 있는데, Blocking/Non-Blocking은 스레드가 개입하는 물리적 구분으로, 스레드A가 스레드B의 동작을 멈추게 하는지 마는지로 구분한다) 이러한 함수형 프로그래밍의 특징을 활용해 데이터 스트림을 비동기적 처리하는 방식이 최근에 많은 각광을 받고 있는 함수형 리액티브 프로그래밍(Functional Reactive Programming)이다. Java 1.8 버전부터는 CompletableFuture의 기능이 추가되어 Java에서도 비동기 프로그래밍을 비교적 쉽게 할 수 있다.
함수형 언어가 각광받는 이유
함수형 프로그래밍이 왜 대세인지 위에 나열한 특징들을 본다고 하더라도 잘 이해가 되지 않을 수 있다. 함수형 언어가 이토록 각광받고 있는 이유는 사실 프로세서 발전의 한계 요인이 가장 크다. 과거 70~80년대부터 몇년 전 까지만 해도 프로세서 하나의 성능은 굉장한 속도로 발전하였다. CPU의 성능이 18개월 마다 2배씩 향상이 된다는 법칙이 있을 정도로 CPU 성능의 발전 속도는 굉장했다. 그러나 최근에 들어서는 발전의 속도가 많이 더디어지고 있어, 프로세서의 성능보다는 프로세서를 넣을 수 있는 개수에 발전의 초점을 맞추고 있다. 즉, 프로세서의 성능보다 개수가 점차 중요해짐에 따라 동시성을 이용한 병렬/분산 프로그래밍이 중요해지고 있고, 동시성 프로그래밍을 부작용 없이 가장 잘 할 수 있는 함수형 프로그래밍이 각광을 받고 있는 것이다. 이렇기 때문에 스칼라, F#, 클로저, 하스켈과 같은 함수형 프로그래밍 언어들이 새롭게 뜨고 있고, 함수형프로그래밍이 아니던 언어Java, C++, C#, JavaScript 등에서도 함수형 프로그래밍을 조금씩 도입하고 있는 것이다.
불과 1-2달 전, 함수형 프로그래밍에 대해 잘 알지 못했을 때는 객체지향 프로그래밍만이 소프트웨어 개발의 정답인 줄만 알았다. 함수형 프로그래밍이 대세를 이끌고 있다는 것을 최근에서야 많이 느껴 이렇게 공부하게 되었는데, 공부를 하면 할수록, 현재 확실히 열풍이고, 앞으로 계속해서 배워야 할 기술이라고 느꼈다. 프로그래밍 언어의 패러다임이 명령형 언어 -> 절차지향 언어 -> 객체지향 언어 순서로 변했다는 말이 있다. 이제 다음 시대는 함수형 프로그래밍이고, 지금은 그 과도기가 아닐까? 그렇다면 함수형 프로그래밍이 기존의 객체지향 프로그래밍을 완전히 대체할 수 있을까?
참고 블로그
- 함수형 프로그래밍이란 무엇인가? (https://medium.com/@jooyunghan/함수형-프로그래밍이란-무엇인가-fab4e960d263#.8fi0a5yfs)
- 어떤 프로그래밍 언어들이 함수형인가? (https://medium.com/@jooyunghan/어떤-프로그래밍-언어들이-함수형인가-fec1e941c47f#.8mrcbbcil)
- 나무위키- 프로그래밍 언어 (https://namu.wiki/w/프로그래밍 언어#s-5.3.1)
- [번역] 함수형 프로그래밍(Functional Programming) 기초 (http://kwangshin.pe.kr/blog/2013/01/21/)