[왜 클로저(Clojure)인가?] 2. 클로저(Clojure)의 본질적 우수성
로제타 코드
RosettaCode.org라는 프로그래밍 코드 선집(programming chrestomathy) 싸이트가 있다 (이름은 Rosetta Stone에서 따왔는데, 이것은 고대 이집트의 프톨레마이오스 5세의 포고문 내용을 고대 이집트의 신성문자와 민중문자, 그리고 그리스어 등 3개 언어로 적어 넣은 비석의 조각으로, 이집트 상형문자 해독에 결정적인 열쇠가 되었다). 이 싸이트는 하나의 프로그래밍 문제에 대해 다양한 프로그래밍 언어로 풀이 코드를 적어 넣는 위키인데, 같은 문제에 대해 다양한 언어간 풀이 코드를 비교해 볼 수 있어, 다른 언어를 공부하는데 좋은 도구가 된다. 현재 690개의 문제가 515개의 언어로 코딩되어 있다.
Jon McLoone은 로제타 코드에 제출된 언어들의 코드의 길이를 비교 분석했다 (Code Length Measured in 14 Languages). 문제풀이 코드들의 문자 수, 토큰 수, 라인 수 등 여러 가지 지표를 사용해서 비교 분석하면서 사실 그가 보여주고자 했던 것은 Mathematica 코드가 가장 짧다는 것이었고 결과는 그렇게 나왔다. 그런데 재미있는 것은, 그가 의도했던 것은 아니었겠지만, 그의 분석 결과가 클로저(Clojure)는 Mathematica 다음으로 코드가 짧다는 것을 보여준다는 사실이다 (심지어 모든 문제들에 대한 풀이 코드 비교에서 라인 수는 Mathematica와 클로저가 같다!). 다음은 로제타 코드에서 인기있는 (문제풀이 코드가 많이 제출된) 14개 언어들을 대상으로 큰(복잡한) 문제(Large-Tasks)에 대한 각 언어별 풀이 코드의 라인 수의 상대적 비율을 보여주는 표이다.
![RosettaCodeLengthRatio](https://storage.googleapis.com/cdn.media.bluedot.so/bluedot.techsuda/wp-content/uploads/2013/10/RosettaCodeLengthRatio-500x378.jpg)
이 표를 보면 Mathematica가 역시 가장 코드 라인 수가 작다 (Mathematica는 범용 언어가 아니다). 맨 위 노란줄은 Mathematica를 1로 했을 때 다른 언어들의 코드 라인수의 상대적인 비율을 보여준다. C는 17배 길고, C++는 9.1배, Java는 6.4배로 긴데, 클로저(Clojure)는 1.6배로 가장 작다. 또한 클로저(Clojure)와 다른 언어를 비교한 것을 보면(빨간 가로 직사각형) 그 비율이 1.0 이하인 것은 없다는 것을 알 수 있는데, 이는 클로저(Clojure)보다 짧은 언어는 없다는 것을 나타낸다. 빨간 세로 직사각형를 보면 클로저(Clojure)와 루비(Ruby), Haskell, Pacal과의 비교를 볼 수 있는데, 모두 1.0 이하인 것을 보면 클로저(Clojure)의 코드 길이가 더 짧다는 것을 알 수 있다.
다른 지표들(토큰 수와 문자 수)에서도 역시 클로저 코드는 Mathematica 다음이었다.
클로저(Clojure)의 막강한 표현력
언어는 표현 양식이다. 프로그래밍 언어는 문제를 기술하고 해결하는 자동화된 과정에 대한 표현 양식이다. 이것이 본질적인 부분이다. 얼마나 잘 표현하는가. 그 언어의 표현력!
프로그래밍 언어의 표현력이 좋다는 것은 문제를 기술하고 해결하는 전체적인 과정에서의 총 인지적 사고 비용이 적게 들어야 한다는 것을 말한다. 어떤 프로그래밍 언어의 표현력이 좋다는 것을 알 수 있는 단순한 지표는 코드의 길이이다. 다음과 같은 이유에서다.
- 코드가 짧을수록 코드를 작성하고 독해하는데 드는 인지적 비용이 적다.
- 코드가 짧을수록 추상성 구축이 쉽다.
- 코드가 짧을수록 변경이 쉽다.
- 코드가 짧을수록 버그가 적다.
지나치게 단순화한 것인지 모르겠지만 가독성을 크게 헤치지 않는 선에서 코드가 짧은 프로그래밍 언어일수록 좋은 언어라는 것이다.
(이 글에서 나는 프로그래밍 언어의 표현력을 그 언어의 본질적 우수성으로 정한다. 반면 우연적 우수성은 실행 속도, 라이브러리, 프레임워크, 개발자 커뮤니티, 벤더의 지원 등이다. 실행 속도가 우연적 우수성인 이유는 그것은 그 언어가 아니라 그 언어가 컴파일된 코드 혹은 인터프리터의 컴퓨터에서의 수행 속도로서 언어 자체의 속성은 아니기 때문이다 (구글의 자바스크립트 엔진 V8(8기통 엔진?)을 보라). 라이브러리나 프레임워크는 개발하기 나름이다 . 이러한 나의 생각은 SICP의 '절차 인식론'에 기반하는 것이다 - 우연적 우수성이라 해서 중요하지 않다는 것은 아니다. 오히려 개발 언어의 선택에 있어 우연적 우수성은 중요한 위치를 자치한다.)
이제 실제 코드를 보자. 좀 실용적인 예제 코드로 비교해 보는 것이 좋을 것이다. 지난 회에서 소개했던, ‘Programming Scala’와 ‘Programming Hive’의 저자이기도 한, 딘 왐플러(Dean Wampler)가 2013년 8월 3일 함수형 프로그래머를 위한 회의인 Lambda Jam 2013에서 ‘Copious Data: The “Killer App” for Functional Programming’ 이란 주제로 발표했다 (그는 ‘Big Data’를 ‘Copious Data’로 바꿔 불러야 한다고 한다). 이 발표에서 그는 빅데이타에서 Map/Reduce 보다 더 좋다고 소개하는 Cascading에서 ‘단어세기(word count)’ 알고리즘을 구현한 Java, Scala, Clojure 코드를 비교했다 (코드 길이 비교가 아니더라도 빅데이타 분야에 종사하시거나 관심이 있으신 분들은 그의 발표를 한 번 깊이 있게 생각해 보는 것이 도움이 되지 않을까 한다).
![code-compare](https://storage.googleapis.com/cdn.media.bluedot.so/bluedot.techsuda/wp-content/uploads/2013/10/code-compare-500x220.jpg)
위 코드를 당장 이해할 필요는 없기 때문에 단지 코드의 형태만 10000 피트 상공에서 감상해 보자. 어느 쪽이 더 단순해 보이는가? 위의 ‘단어 세기’ 알고리즘을 구현한 코드는 각각 자바(Java)는 28줄, 스칼라(Scala)는 17줄, 클로저(Clojure)는 7줄이다. 프로그래밍을 전혀 모르는 사람이라도 같은 기능을 하는 코드라면 어떤 것이 더 좋다고 생각할까?
클로저 코드가 저렇게 짧을 수 있는 이유는 매크로라는 기능을 이용해서 Datalog의 표기 방식을 DSL로 구현했기 때문이다. 위의 클로저 코드는 클로저의 Cascalog 라이브러리를 사용한 것인데, 이것은 SQL과 같은 데이타 처리 언어인 Datalog의 표기 방식으로 Cascading의 로직을 구현한 것이다.
한가지 예를 더 보자. 게임에서 사용하는 미로를 임의적으로 만들어내는 알고리즘을 미로 발생기 알고리즘이라고 하는데, 여러 가지로 개발되어 있다. 그중 윌슨의 미로 발생기 알고리즘은 다음과 같다.
윌슨의 미로 발생기 알고리즘 :
- 임의의 좌표를 골라서 방문 표시한다.
- 방문하지 않은 임의의 좌표를 고른다 - 만약 없다면 미로 출력
- 2에서 고른 좌표에서 출발하여 방문 표시된 위치까지 랜덤 걷기한다 - 랜덤 걷기 중 한 위치를 2번 이상 지난다면, 항상 그 방향을 기억한다.
- 랜덤 걷기한 모든 위치를 방문 표시하고, 출구 방향에 따라 벽을 제거한다.
- 2로 돌아간다.
아래는 윌슨 알고리즘을 클로저로 구현한 코드이다. (Clojure Programming p.147에서 인용)
(defn maze [walls]
(let [paths (reduce (fn [index [a b]]
(merge-with into index {a [b] b [a]}))
{} (map seq walls))
start-loc (rand-nth (keys paths))]
(loop [walls walls
unvisited (disj (set (keys paths)) start-loc)]
(if-let [loc (when-let [s (seq unvisited)] (rand-nth s))]
(let [walk (iterate (comp rand-nth paths) loc)
steps (zipmap (take-while unvisited walk) (next walk))]
(recur (reduce disj walls (map set steps))
(reduce disj unvisited (keys steps))))
walls))))
이 코드를 당장 이해할 필요는 없다. 아래는 윌슨의 미로 발생기 알고리즘을 구현한 클로저 코드와 자연어(한글) 기술을 라인별 일대일 매칭 관계를 보여준다.
클로저 구현 코드 | 자연어(한글) 기술 | |
1줄 | (defn maze [walls] | X |
2줄 | (let [paths (reduce (fn [index [a b]] | X |
3줄 | (merge-with into index {a [b] b [a]})) | X |
4줄 | {} (map seq walls)) | X |
5줄 | start-loc (rand-nth (keys paths))] | 윌슨 1: 임의의 좌표를 골라서 |
6줄 | (loop [walls walls | X |
7줄 | unvisited (disj (set (keys paths)) start-loc)] | 윌슨 1: 방문 표시한다. |
8줄 | (if-let [loc (when-let [s (seq unvisited)] (rand-nth s))] | 윌슨 2: 방문하지 않은 임의의 좌표를 고른다. |
9줄 | (let [walk (iterate (comp rand-nth paths) loc) | 윌슨 3: 랜덤 걷기 수행. |
10줄 | steps (zipmap (take-while unvisited walk) (next walk))] | 윌슨 3: 방문 표시된 위치까지 |
11줄 | (recur (reduce disj walls (map set steps)) | 윌슨 4: 벽 제거 |
12줄 | (reduce disj unvisited (keys steps)))) | 윌슨 4: 방문 표시한다. |
13줄 | walls)))) | 윌슨 2: 만약 없다면 미로 출력 |
위 표에서 X로 표시된 것은 클로저 코드 라인중에서 윌슨의 알고리즘을 자연어(한글)로 표기한 문장에서 표현되지 않은 것을 나타내는데, 단지 전체 클로저 코드 라인 13개 라인중에서 5개 뿐이다. 즉 자연어와 무관하게 프로그래밍 언어 그 자체만을 위한 코드 라인은 5개 뿐이고, 나머지 8개 라인은 자연어의 표현을 그대로 클로저 코드로 표현하고 있는 것이다. 이것은 클로저 코드는 인간의 자연어에 비견되는 수준의 추상화 로 프로그래밍 코드를 만들어 낼 수 있다는 것을 보여준다.
이처럼 클로저는 그 표현력이 매우 뛰어나고 코드의 길이가 매우 짧다. (함수형 언어를 접해본 경험이 없는) 자바나 C++ 프로그래머들이 클로저를 공부하면서 느끼게 되는 것은 ‘어떻게 이렇게 짧은 코드로 그 많은 것을 표현할 수 있지? 도대체 이 코드들에는 버그가 들어설 자리조차 없어 보인다’ 라는 것이다.
클로저의 이러한 특성은 복잡성을 제거하고 단순성을 최대화하는 것에 역점을 두고 프로그래밍 언어 설계시에 언어의 구성에 필요한 패러다임 요소들을 선택했기 때문이다.
클로저 언어 창시자인 리치 히키(Rich Hickey)는 한 인터뷰에서 이렇게 말했다.
“복잡성은 저의 1차적 관심사인데요, 제 생각에 클로저는 이것을 잘 처리하고 있다고 생각합니다. 코어 수준에서 클로저는 매우 단순합니다. 클로저는 명백한 복잡성과 우연적 복잡성 둘 다 줄이는데 집중합니다. 특히 후자는 문제 영역이 아닌 해결 영역-언어나 도구-에서 나오는 복잡성입니다. 제 생각에 프로그래머들이 우연적 복잡성에 익숙해져 있는데요, 특히 단순한 것을 친숙한 것 혹은 간결한 것과 혼동하고 있지요. 그래서 복잡성을 만나면, 그것을 극복해야 할 도전 과제로 여깁니다. 사실 그건 제거해야 할 장애물인데요. 복잡성을 극복하는 것은 잘 돼지 않습니다. 그건 낭비입니다.”
다음 회에서는 단순성과 복잡성에 대한 이야기를 하려 한다. 좀 추상적이고 철학적인 이야기인데, 정말 중요한 이야기다. 왜냐하면 이것은 단지 클로저에 대한 것만이 아닌 프로그래밍 전반에 있어 소프트웨어 역사상 가장 중요한 개념이기 때문이다.