- Learn about Wiki
- Lectures
- Study
- Tips
무어의 법칙에 따르면 컴퓨터의 성능은 18개월마다 2배씩 증가한다. 기존까지는 이것이 하나의 칩에 더 많은 트랜지스터를 집적하고 클럭 속도를 증가시킴으로서 가능했다. 하지만 마이크로프로세서 제작의 기술적 한계1)에 의해 속도 증가는 불가능하게 되었다. 이제 무어의 법칙은 다른 방식으로 성립되는데 최근에는 한 칩에 여러 개의 코어를 넣어서 컴퓨팅 성능을 향상하고 있다. 바야흐로 멀티코어의 시대인 것이다!
마이크로프로세서 자체의 속도가 빨라지던 시대에는 프로그래머는 18개월마다 소프트웨어가 2배씩 빨라지는 이점을 공짜로 얻었다. 하지만 멀티코어의 시대가 되면서 프로그램의 속도를 올리기 위해서는 여러 개의 코어에 작업을 직접 할당해야만 한다. 즉 성능 향상을 위해서는 멀티스레딩 프로그래밍 작업을 해야 하는 것이다2). Herb Sutter는 이를 두고 공짜점심은 끝났다 라고 하였다.
IT 산업에서 멀티코어 시대라는 환경의 변화는 멀티스레딩 프로그래밍이라는 강력한 진화압으로 작용하고 있다. 이제 살아남기 위한 여러 가지 새로운 기술들이 여기 저기서 나타날 것이고, 도태와 적자생존이라는 또 한편의 게임이 시작된 것이다.
멀티스레드 프로그래밍은 어렵다. 대부분의 프로그래머들이 스레드를 다룰 때 어려움을 호소한다. 아무리 뛰어난 프로그래머라 하더라도 100% 정확하게 동작하는 높은 병렬성을 가진 멀티스레딩 코드를 작성하는 것은 불가능하다. Unreal이라는 게임 엔진을 만든 천재 프로그래머 Tim Sweeney조차 멀티스레딩 프로그래밍의 어려움을 호소했다.3) 이 어려움을 어떻게 극복할 것인가가 당연히 많은 선도적인 프로그래머들의 고민이 되었다.
No Silver Bullet 라는 기사에서 Fred Brooks는 소프트웨어 개발에서의 우연적 복잡성과 본질적 복잡성을 구분하면서, 우연적 복잡성의 해결은 고수준 언어의 역할이라고 했다. 당시에는 포트란이 어셈블리 언어에 내재되어 있는 우연적 복잡성을 제거하는 것으로 각광을 받았다. 그리고 자바와 같은 객체지향 프로그래밍 언어들이 C와 같은 절차형 언어를 그렇게 대체했다. 하지만 이제 시대는 멀티스레딩 프로그래밍의 어려움을 야기하는 우연적 복잡성에 직면해 있다.
그래서 요즘 주목받는 것이 함수형 프로그래밍이다. Common Lisp, Scheme, Haskell, Erlang 에 대한 관심도 그렇지만 F#, Clojure, Scala 등 현대적인 함수형 언어들이 등장하고 있다. 또한 객체지향 언어인 Java4)나 C++5) 등의 최신 버전에는 함수형 언어의 일부 기능을 속속 소개하고 있다. Javascript6)도 마찮가지다. 이렇게 주목받는 이유는 함수형 언어에서는 모든 데이타가 기본적으로 불변 데이타인데, 불변 데이타는 스레드가 아무리 많아도 그 접근에 아무 문제를 일으키지 않는다는 점이다. 함수형 언어에서는 멀티스레딩을 위해 Lock같은 접근제어 기능이 필요 없어 데드락이 발생하지 하지 않으며, 어느 스레드가 어떤 변수를 건드렸는지 다른 스레드가 고민할 필요가 전혀 없다는 점도 있다
함수형 프로그래밍 언어의 역사는 John McCarthy가 처음 Lisp을 만들던 1958년으로 거슬러 올라간다. 이후 Scheme (1975), Erlang (1986), Haskell (1990), Common Lisp (1994), OCaml (1996) 등으로 이어졌다. 1990년대와 2000년대 초반까지는 변방에서 명맥을 유지하다가 새롭게 부활하여 Scala (2003), F# (2005), Clojure (2007) 등의 VM 기반의 현대적인 프로그래밍 언어들이 탄생하게 되었다.
이처럼 많은 함수형 언어들 중 특히 클로져가 주목받는 이유는 무엇일까? 이에 대해 알기 위해 매우 많은 논쟁을 일으킨 Robert C. Martin의 The Last Programming Language7)라는 블로그 글을 읽어볼 필요가 있다. 그의 요지는 우리가 이제 거의 발명할 수 있는 모든 언어를 다 발명하였고, 새로운 언어는 뭔가 혁신적인 것이 아니라, 기존의 것을 약간 수정한 수준일 뿐이라는 것과, IT 분야가 현재의 기예의 단계에서 과학의 단계로 발전하기 위해서는 수학이나 화학에서처럼 단일한 언어를 가질 필요가 있다는 것이다. 이 최후의 프로그래밍 언어는 어떤 것일까? Robert C. Martin는 한 키노트에서 이에 대해 다음과 같이 말했다고 한다.
최후의 프로그래밍 언어는
이런 기준에 의하면 일단 가상 머신에서 실행되어야 하기 때문에, 기존 함수형 언어들은 제외된다. 그는 이러한 조건을 만족하는 언어는 클로저라면서 Why Clojure?8)에서 다음과 같이 말했다.
같은 글에서 그는 다음과 같이 말하면서 클로저에 대한 장밋빛 전망을 내놓았다.
“우리는 지금까지 수십년에 걸쳐 프로시져(Procedure절차형)에서 객체로 이전해 왔다. 이제 하드웨어의 물리적 제약이 비슷한 식으로 함수형 언어로의 패러다임 이동으로 우리를 내몰고 있다. 다음 몇 년 동안 우리는 어느 함수형 언어가 가장 좋은지 판별할 수 있는 다양한 프로젝트 실험들을 보게 될 것이다. 그 실험의 결과가 나올 때 나는 클로져가 매우 높은 위치를 차지할 것이라는 것을 완전 기대한다.”
클로저는 Rich Hickey에 의해 만들어 졌다. 그는 한 인터뷰에서 클로저를 만들게 된 동기에 대해 이렇게 말했다.
“우리는 기존의 객체지향 언어에 내재되어 있는 우연적 복잡성 때문에 매우 힘듭니다. 그 복잡성은 문법적인 것도 있고, 의미론적인 것도 있는데, 제 생각에 우리가 이런 사실을 전혀 모르고 있다는 거에요. 저는 '올바른 일을 한다는 것(doing the right thing)'9)이 단지 관례나 훈련에 의해 되는 것이 아니라, 저절로 자연스럽게 되게 하고 싶었어요. 저는 강고한 동시성과 기존 자바 라이브러리와의 거대한 상호운영성을 원했습니다.”
“복잡성은 저의 1차적 관심사인데요, 제 생각에 클로저는 이것을 잘 처리하고 있다고 생각합니다. 코어 수준에서 클로저는 매우 단순합니다. 클로저는 명백한 복잡성10)과 우연적 복잡성 둘 다 줄이는데 집중합니다. 특히 후자는 문제 영역이 아닌 해결 영역-언어나 도구-에서 나오는 복잡성입니다. 제 생각에 프로그래머들이 우연적 복잡성에 익숙해져 있는데요, 특히 단순한 것을 친숙한 것 혹은 간결한 것과 혼동하고 있지요. 그래서 복잡성을 만나면, 그것을 극복해야 할 도전과제로 여깁니다. 사실 그건 제거해야 할 장애물인데요. 복잡성을 극복하는 것은 잘 돼지 않습니다. 그건 낭비입니다.”
객체지향 프로그래밍은 명사의 왕국(Kingdom of Nouns)이고, 함수형 프로그래밍은 동사의 왕국(Kingdom of Verb)이다. 명령형 프로그래밍(Imperative Programming)에서처럼 객체지향 프로그래밍에서 함수는 모든 일을 함에도 불구하고, 그 지위는 단지 Subroutine (필요에 의해 나열된 명령들이 묶인 것)이라는 2등 시민에 불과하다. 함수형 프로그래밍에서는 데이타와 같은 1등 시민 (런타임에도 생성되며, 함수의 인자로 전달되고 함수의 결과로서 리턴되며, 변수에 할당되는 것(entity))이라는 지위를 부여받은 함수는 조합가능성 (Composablility)이라는 능력을 획득하여, 명령행 프로그래밍에서는 발휘할 수 없었던 (혹은 어렵게 구현해야 하는) 놀라운 기능 (고계함수 (HOF, Higher-order function)을 이용한 Closure, Partial application, Currying 등)을 아주 쉽게 해낼 수 있게 된다.
함수형 프로그래밍에서 함수는 수학에서의 함수 (예를 들어 sin함수)와 같은 의미인데 이를 순수 함수라고 한다. 순수 함수는 다음과 같은 특징을 갖는다.
프로그램의 상태는 프로그램의 코드에 의해 변경될 수 있는 데이타이다. 그런데 만약 코드의 결과가 상태에 의존한다면 모든 가능한 상태에 대해 코드의 행위를 추적한다는 것은 매우 어려운 작업이 되는데, 왜냐하면 프로그램의 다른 코드들의 부수 효과로 인해 그 상태는 변경되기 때문이다. 더욱이 동시성 문제가 개입되면 코드들의 실행 순서를 잡는 것은 쉽지 않아, 복잡성은 더 겉잡을 수 없는 수준이 된다.
순수 함수를 사용하면 이러한 사태는 없어진다. 순수 함수는 프로그램의 상태를 바꾸지 않으며, 그 자체로는 상태가 없다고 할 수 있다.
search?q=Pure%20and%20non-pure%20Functions.svg&btnI=lucky
순수 함수의 이점은 다음과 같다.
캡슐화와 재사용성은 OOP에서 객체를 통해 지원하려 했지만 완벽하게 보장할 수 없는 기능이다. 왜냐하면 객체는 자체의 상태를 갖고 있기 때문에, 메소드의 호출은 객체의 상태에 따라 달라지게 되는데, 이는 결국 외부에 어떤 식으로든 그 상태를 노출하게 된 것이 된다. 또한 상태를 갖는 객체는 구성가능하지 않다. 결국 객체를 구성하면서 만들어진 시스템은 매우 취약한 시스템이 된다. (HTTP는 요청과 응답으로 이루어진 Statelss protocol이었다는 사실에 주목하라)
프로그램이 순수 함수만으로 만들어 질 수는 없다. 화면에 표시하거나. 파일을 읽거나, 넷트웍으로 데이타 송수신을 하는 등의 부수 효과는 반드시 필요하다. 클로저는 함수가 반드시 순수 함수이기를 강제하지 않는다.(Haskell 같은 순수 함수형 언어에서는 모든 함수는 순수 함수이다. Haskell에서 I/O는 Monad를 통해 이루어진다11)). 클로저에서는 다음과 같은 타협점을 취한다.
클로저에서 모든 데이타는 불변 데이타이다. 데이타는 한 번 생성되면 변경 불가능이다. 불변 데이타는 순수 함수와 불가분의 관계이다. 만약 순수 함수의 파라미터가 불변 데이타가 아니라면, 그래서 함수 실행중에 다른 곳에서 파라미터의 값이 변한다면, 그 함수는 순수 함수가 될 수 없다.
불변 데이타의 이점은 다음과 같다.
불변 데이타는 다른 언어에서도 권장되고 있다. Effective Java의 저자 Joshua Bloch는 “클래스는 변화해야 할 분명한 이유가 없다면, 변경 불가능해야 한다. 만약 클래스가 불변일 수 없다면, 그 변경 가능성을 제한해야 한다.”라고 하였다. 실제 Java의 String, Integer, Long등 클래스는 불변 클래스이다. Scott Meyers의 Effective C++의 Item 3는 “Use const whenever possible”이다.
하지만 프로그램에서 데이타를 변화시킬 필요가 있을 때는 어떻게 할까? 클로저는 이것을 존속성 데이타 구조 (Persistent12) Data Structure)로 아주 효과적으로 지원한다. 존속성 데이타 구조란 데이타의 갱신이 기존 데이타의 수정을 통해서가 아니라 기존 데이타를 그대로 유지한 채 신규 데이타와의 차이만을 추가함으로서 이루어지는 데이타 구조를 말한다. 데이타의 갱신은 데이타의 수정이 아니라 데이타의 차이의 추가인 것이다. 이 부분에 대해서는 데이타 구조에서 자세히 소개할 것이지만 간단한 리스트의 경우를 보자.
(def a (list 1 2 3)) (def b (conj a 0))
a 라는 리스트가 만들어 진 후, a에 0을 추가하여 b를 만드는 과정이다. list는 가변 인수를 받아 클로저의 리스트 자료구조를 만든다. 이것은 a라는 심볼로 지정된다. conj는 a 리스트에 0를 추가하여 새로운 리스트를 만들어 내는데, 이것은 심볼 b에 지정한다. 이 과정을 그려보면 다음과 같다.
search?q=perisitent-list.svg&btnI=lucky
리스트 b는 리스트 a와 자료 구조를 공유하고 있다. 리스트 b는 리스트 a의 값을 전부 복사하는 것이 아니다. 리스트 a의 기존 값들에 0를 추가하여 새로운 리스트를 만들어 낸 것이 리스트 b이다.
클로저는 객체지향 언어가 아니다. 그 자신이 오랜 동안 C++과 Java를 사용했던 프로그래머였지만, Rich Hickey는 특히 멀티스레딩 프로그래밍할 때, OOP 언어의 한계를 절감했다고 한다. 앞에서도 설명했듯이 멀티스레딩 프로그래밍의 어려움의 근본적인 원인은 데이타의 변경 가능성에 있는데, 프로그램의 상태를 바꾸는 코드들을 나열하는 명령형 프로그래밍의 한계를 OOP가 극복하지 못하기 때문이다.
데이터의 불변성을 보장하지 않는 것은 비단 OOP 언어들만의 문제는 아니고 지금까지 대부분의 언어들도 마찬가지다. 이것은 시간에 대한 잘못된 인식에 기인하는데, 동시성에서 자세히 살펴볼 것이다.
OOP의 좋은 측면도 있다. 그런 점은 클로저도 지원한다.
클로져는 리스프의 적통이어서 괄호를 사용하여 코드를 작성한다. 다음은 전형적인 클로져 코드이다.
(println (+ 1 2 3 4 5)) ;>> 15 ;=> nil
리스프에 익숙하지 않은 프로그래머들에게는 이러한 표현이 낯설 것이다. 그러나 이 표현은 여러 가지 장점이 있다.
괄호 표현은 자바나 C++의 {}나 파스칼의 begin … end와 같이 스코프를 구분한다. 다만 함수명이나 특수구문(다른 언어서의 제어문)이 괄호 안에 있을 뿐이다. 클로져의 (println “hello”), (if true …)은 자바의 println(“hello”), if(true) {…}와 같다. 다만 함수 println이나 if가 괄호 안에 있느냐 아니면 밖에 있느냐의 차이일 뿐이다.
괄호 표현이 좋은 또 한가지는 연산자 전방표기다. 전방표기가 되지 않는 다른 언어에서는 (1 + 2 + 3 + 4 + 5) 처럼 + 연산자를 여러 번 사용해야 한다.
또 괄호 표현은 연산자 우선순위를 알 필요를 없앤다. 자바에서는 a & b « 3에서 어떤 연산자가 먼저 실행되는지 알아야 할 것이다. 이것은 잠재적인 버그 양산 가능성을 야기한다.
괄호는 리스트를 만들어 내는데 이것은 함수 호출 표현이다. 리스트에서 첫번째 요소는 함수로 취급되고 나머지는 인수가 된다.
괄호 표현이 처음에는 매우 이상하고 낯설겠지만, 일단 익숙해지면 그것이 매우 강력한 표현식이라는 것을 깨닫게 될 것이다. 왜냐하면 괄호 표현은 code as data(곧 설명됨, 즉 코드를 데이타처럼 취급할 수 있다!)라는 또 하나의 강력한 추상화를 가능하게 하기 때문이다.
괄호를 사용하는 이러한 표현 방식은 리스프의 창안자 John McCarthy에 의해 만들어졌는데 이를 s-expression 혹은 sexprs (symbolic Expressions15))라고 한다. S-Expression은 코드와 데이타를 같이 표현하기 위한 간단한 방법인데 다음과 같다.
Atomic Symbol은 영문자와 숫자등의 문자만 이루어진 스트링이다. (A.B)는 특히 Cons Cell이라고 하는데, Orderd Pair를 묶어낸다. 두 쌍이 묶여있는 순서가 중요한데, 처음 자리에 있는 것을 car (여기서는 A), 다음 자리에 있는 것을 cdr (여기서는 B)라고 한다16). S-Expression으로 리스트는 다음과 같이 표현된다.
일반적으로 유효한(평가가능한) s-expression을 문구(form)이라고 한다.18) 즉 (if conditon then else)는 if 문구, [1 2 3]은 벡터 문구이다. 하지만 (1 2 3)은 s-expression 이긴 하지만 문구는 아닌데, 리스트의 첫 요소인 1은 호출될 수 있는 것이 아니어서 평가될 수 없기 때문이다.
search?q=sep-form.svg&btnI=lucky
클로저에서는 코드가 곧 데이타이다. 이를 Code As Data라고 한다. 좀 어려운 말로는 Homoiconicity라고 하는데, 이것은 언어의 데이타를 표현하는 방식과 그 언어의 코드를 표현하는 방식이 같다는 것을 말한다. Lisp 계열 언어에서와 마찬가지로 클로저는 s-expression이라는 방식으로 코드와 데이타를 표현한다. 이것은 강력한 Metaprogramming 을 제공하는데, 클로저의 매크로는 이에 기반한 것이다. 19)
클로저에서는 코드를 s-expression으로 표현하며, 이를 문구(form)이라고 한다. 문구(form)는 어떤 값으로 평가되는 코드이다.
클로저 프로그램의 기본 단위는 문구(form)이다(라인, 키워드, 클래스가 아니다). 문구(form)은 평가되어 값을 리턴할 수 있는 코드 단위이다. REPL에서 뭔가 코드를 넣으면 그것이 평가되어 값을 리턴한다. 클로저 소스 파일은 문구(form)으로 구성된다. 문구(form)에는 4 종류가 있다.
다른 언어에서는 if, for, break 등은 프로그램의 동작을 제어할 뿐이지 어떤 값으로 평가되지 않는 반면, 클로져에서는 항상 어떤 값으로 평가되는 특수 문구(special form)이다.
(if true :a :b) ;=> :a
심지어 값도 항상 평가되는데, 자기 자신으로 평가된다.
43 ;=> 43 [1 2 3] ;=> [1 2 3]
물론 연산자는 연산 결과로 평가된다.
(+ 1 2 3) ;=> 6
심볼의 정의는 심볼로 평가된다.
(def a 13) ;=> #'user/a 'a ;=> a (defn average [& numbers] (/ (apply + numbers) (count numbers))) ;=> #'user/average 'average ;=> average
그리고 심볼은 그 값으로 평가된다.
a ;=> 13 average ;= #<user$average user$average@121f653>
물론 함수의 호출은 함수의 리턴값으로 평가된다.
(average 3 4 5 6 7) ;=> 5
Clojure는 JVM상에서 실행된다. Clojure 코드를 컴파일하면 JVM상에 실행할 수 있는 .Class 파일이 된다. (클로저는 닷넷에서도 돌아간다(ClojureCLR). 브라우져에서도 돌아간다(ClojureScript).)
Clojure 프로그램에서는 기존의 자바 라이브러리를 바로 사용할 수가 있다. 기존 자바 코드에서 했던 것을 어떤 랩핑 작업없이 바로 클로저 코드해서 할 수 있다.