프로그래밍 언어의 힘, Lisp의 독특함, 그리고 평균에 안주하는 기술 선택의 비용에 대한 에세이.

2002년 5월
"우리가 노린 것은 C++ 프로그래머들이었다. 우리는 그들 중 많은 사람을 Lisp 쪽으로 반쯤 끌어오는 데 성공했다."
소프트웨어 업계에는 뾰족한 머리의 학자들과, 그에 못지않게 만만치 않은 또 다른 세력인 뾰족한 머리카락의 상사 사이의 지속적인 싸움이 있다. 뾰족한 머리카락의 상사가 누군지는 모두가 안다, 그렇지 않은가? 기술 업계의 대부분 사람들은 이 만화 속 인물을 알아볼 뿐 아니라, 자기 회사에서 그 인물이 누구를 모델로 했는지도 알고 있다고 생각한다.
뾰족한 머리카락의 상사는 각각 따로는 흔하지만 함께 있는 경우는 드문 두 가지 성질을 기적처럼 결합한다. (a) 그는 기술에 대해 전혀 아무것도 모르고, (b) 그럼에도 그것에 대해 아주 강한 의견을 갖고 있다.
예를 들어 어떤 소프트웨어를 작성해야 한다고 하자. 뾰족한 머리카락의 상사는 그 소프트웨어가 어떻게 동작해야 하는지 전혀 모르고, 프로그래밍 언어도 구분하지 못한다. 그런데도 어떤 언어로 써야 하는지는 알고 있다. 정확히 말해 그는 Java로 써야 한다고 생각한다.
왜 그렇게 생각할까? 뾰족한 머리카락의 상사의 머릿속을 들여다보자. 그의 생각은 대략 이렇다. Java는 표준이다. 언론에서 항상 읽으니 틀림없이 표준일 것이다. 표준이니 그걸 써도 내가 문제를 뒤집어쓸 일은 없다. 그리고 그것은 또 Java 프로그래머가 항상 많이 있다는 뜻이기도 하다. 그래서 지금 나를 위해 일하는 프로그래머들이, 이상하게도 늘 그렇듯이, 그만두더라도 쉽게 대체할 수 있다.
이 말은 그다지 비합리적으로 들리지 않는다. 하지만 전부 한 가지 암묵적 가정 위에 서 있고, 그 가정은 거짓으로 드러난다. 뾰족한 머리카락의 상사는 모든 프로그래밍 언어가 대체로 동등하다고 믿는다. 그게 사실이라면 그는 정확히 맞았을 것이다. 언어가 모두 동등하다면, 물론 남들이 다 쓰는 언어를 쓰면 된다.
하지만 모든 언어가 동등한 것은 아니다. 그리고 나는 언어들 사이의 차이점으로 들어가지 않고도 이것을 증명할 수 있다고 생각한다. 만약 1992년에 뾰족한 머리카락의 상사에게 소프트웨어를 어떤 언어로 써야 하느냐고 물었다면, 그는 오늘만큼이나 주저 없이 대답했을 것이다. 소프트웨어는 C++로 써야 한다고. 그런데 언어가 모두 동등하다면, 왜 그의 의견은 바뀌었을까? 사실 Java 개발자들은 왜 새 언어를 만들 귀찮은 일을 했을까?
아마도 새 언어를 만든다면, 사람들이 이미 갖고 있던 것보다 어떤 면에서 더 낫다고 생각했기 때문일 것이다. 실제로 Gosling은 첫 Java 백서에서 Java가 C++의 몇 가지 문제를 고치도록 설계되었다고 분명히 말한다. 그러니 결론은 이것이다. 언어는 모두 동등하지 않다. 뾰족한 머리카락의 상사의 머릿속에서 Java까지 따라가고, 다시 Java의 역사에서 그 기원까지 거슬러 올라가면, 처음 출발할 때의 가정과 모순되는 생각을 손에 쥐게 된다.
그렇다면 누가 맞는가? James Gosling인가, 아니면 뾰족한 머리카락의 상사인가? 놀랄 것도 없이 Gosling이 맞다. 어떤 문제들에 대해서는 어떤 언어가 다른 언어보다 더 낫다. 그리고 여기서 흥미로운 질문들이 생긴다. Java는 어떤 문제들에 대해 C++보다 더 낫도록 설계되었는가? 언제 Java가 더 좋고 언제 C++가 더 좋은가? 둘 중 어느 쪽보다도 다른 언어가 더 나은 상황도 있는가?
이 질문을 진지하게 고려하기 시작하면 진짜 골칫거리를 열어젖힌 셈이다. 뾰족한 머리카락의 상사가 문제의 복잡성을 온전히 생각해야 한다면 머리가 폭발해버릴 것이다. 모든 언어가 동등하다고 생각하는 한, 그가 해야 할 일은 가장 기세가 있어 보이는 것을 고르는 것뿐이다. 그리고 그것은 기술보다는 유행의 문제에 더 가깝기 때문에, 그조차도 아마 정답을 맞힐 수 있다. 하지만 언어마다 차이가 있다면, 그는 갑자기 자신이 아무것도 모르는 두 가지에 대해 동시에 최적화를 해야 한다. 즉 자신이 해결해야 할 문제에 대해 주요 언어 스무 개 남짓의 상대적 적합성과, 각 언어마다 프로그래머와 라이브러리 등을 구할 가능성 사이의 최적 균형을 찾아야 한다. 문 저편에 그런 것이 있다면, 뾰족한 머리카락의 상사가 그 문을 열고 싶어 하지 않는 것도 놀랄 일은 아니다.
모든 프로그래밍 언어가 동등하다고 믿는 것의 단점은 그것이 사실이 아니라는 점이다. 하지만 장점은 삶을 훨씬 단순하게 만들어준다는 점이다. 그리고 나는 이 생각이 널리 퍼진 주된 이유가 그것이라고 본다. 그것은 편안한 생각이다.
Java는 멋지고 새로운 프로그래밍 언어이니 분명 꽤 괜찮을 것이다. 정말 그럴까? 프로그래밍 언어 세계를 멀리서 보면 Java가 최신 유행처럼 보인다. (충분히 멀리서 보면 Sun이 돈을 들여 세운 크고 번쩍이는 광고판만 보인다.) 하지만 가까이서 보면 멋짐에도 درجات가 있다는 것을 알게 된다. 해커 하위문화 안에서는 Perl이라는 또 다른 언어가 Java보다 훨씬 더 멋진 것으로 여겨진다. 예를 들어 Slashdot은 Perl로 만들어진다. 나는 그 사람들이 Java Server Pages를 쓰는 모습을 상상하기 어렵다. 하지만 더 새롭고 Python이라는 또 다른 언어가 있는데, 그 사용자들은 Perl을 얕보는 경향이 있고, 대기 중인 것은 그것만이 아니다. 더 많이 있다.
이 언어들을 순서대로, Java, Perl, Python으로 놓고 보면 흥미로운 패턴이 눈에 띈다. 적어도 Lisp 해커라면 그 패턴이 보인다. 각각이 점점 더 Lisp와 비슷해진다. Python은 많은 Lisp 해커들이 실수라고 여기는 특징들까지 베껴온다. 단순한 Lisp 프로그램은 줄 단위로 Python으로 번역할 수 있다. 지금은 2002년이고, 프로그래밍 언어들은 거의 1958년을 따라잡았다.
수학을 따라잡기
내가 의미하는 것은, Lisp가 1958년에 John McCarthy에 의해 처음 발견되었고, 대중적인 프로그래밍 언어들이 그가 그때 발전시킨 아이디어들을 이제서야 따라잡고 있다는 것이다.
그런데 어떻게 그게 사실일 수 있을까? 컴퓨터 기술은 매우 빠르게 변하는 것이 아닌가? 1958년의 컴퓨터는 냉장고만 한 거대한 괴물에 손목시계 수준의 처리 능력을 갖고 있었다. 그렇게 오래된 기술이 어떻게 여전히 의미가 있을 뿐 아니라, 최신 기술보다 더 우수할 수 있을까?
어떻게 그런지 말해보겠다. 그것은 Lisp가 사실 프로그래밍 언어가 되도록 설계된 것이 아니었기 때문이다. 적어도 오늘날 우리가 그 말로 의미하는 뜻에서는 그렇다. 우리가 프로그래밍 언어라고 할 때는 컴퓨터에게 무엇을 하라고 지시하는 데 쓰는 것을 뜻한다. McCarthy도 결국 그런 의미의 프로그래밍 언어를 개발하려고 했지만, 실제로 우리가 갖게 된 Lisp는 그가 이론적 연습으로 따로 했던 작업에 기반하고 있었다. 즉 Turing Machine의 더 편리한 대안을 정의하려는 시도였다. 나중에 McCarthy는 이렇게 말했다.
Lisp가 Turing machine보다 더 깔끔하다는 것을 보여주는 또 다른 방법은 보편 Lisp 함수를 작성하고, 그것이 보편 Turing machine의 설명보다 더 짧고 이해하기 쉽다는 것을 보이는 것이었다. 이것이 Lisp 함수 eval이었다. 이 함수는 Lisp 식의 값을 계산한다.... _eval_을 작성하려면 Lisp 함수를 Lisp 데이터로 표현하는 표기법을 발명해야 했고, 그런 표기법은 논문을 위한 목적으로 고안되었을 뿐 실제로 Lisp 프로그램을 표현하는 데 쓰일 것이라는 생각은 없었다.
그다음에 일어난 일은, 1958년 말 어느 때쯤 McCarthy의 대학원생 중 한 명이던 Steve Russell이 이 eval 정의를 보고, 이것을 기계어로 번역하면 결과가 Lisp 인터프리터가 되리라는 사실을 깨달은 것이었다. 이것은 당시로서는 큰 놀라움이었다. 나중에 인터뷰에서 McCarthy는 이렇게 말했다.
Steve Russell이 말하길, 보세요, 제가 이 _eval_을 프로그램으로 짜면 어떨까요..., 그래서 나는 그에게 하하, 자네는 이론과 실천을 혼동하고 있네, 이 _eval_은 읽기 위한 것이지 계산을 위한 것이 아니야, 라고 했다. 하지만 그는 그대로 해버렸다. 즉 그는 내 논문 속 _eval_을 [IBM] 704 기계어 코드로 컴파일하고 버그를 고친 다음, 이것을 Lisp 인터프리터라고 알렸다. 실제로 그것은 분명 Lisp 인터프리터였다. 그래서 그 시점에 Lisp는 본질적으로 오늘날의 형태를 갖게 되었다....
갑자기, 아마 몇 주 사이였을 것이다, McCarthy는 자신의 이론적 연습이 실제 프로그래밍 언어로 변신한 것을 보게 되었다. 그리고 그것은 그가 의도했던 것보다 더 강력한 언어였다. 그래서 이 1950년대 언어가 왜 구식이 아닌지에 대한 짧은 설명은, 그것이 기술이 아니라 수학이었기 때문이라는 것이다. 수학은 낡지 않는다. Lisp를 비교해야 할 올바른 대상은 1950년대 하드웨어가 아니라, 예컨대 1960년에 발견되어 지금도 가장 빠른 범용 정렬인 Quicksort 알고리즘 같은 것이다.
1950년대에서 아직 살아남은 또 하나의 언어는 Fortran인데, 그것은 언어 설계에 대한 정반대의 접근을 대표한다. Lisp는 뜻밖에 프로그래밍 언어로 바뀐 이론의 산물이었다. Fortran은 의도적으로 프로그래밍 언어로 개발되었지만, 오늘날 기준으로 보면 매우 저수준의 언어였다.
Fortran I는 1956년에 개발된 언어로, 오늘날의 Fortran과는 매우 다른 동물이었다. Fortran I는 거의 수학이 붙은 어셈블리 언어였다. 어떤 면에서는 더 최근의 어셈블리 언어보다도 덜 강력했다. 예를 들어 서브루틴이 없고 분기만 있었다. 오늘날의 Fortran은 이제 Fortran I보다 오히려 Lisp에 더 가깝다고까지 말할 수 있다.
Lisp와 Fortran은 서로 다른 두 진화 계통수의 줄기였다. 하나는 수학에, 다른 하나는 기계 구조에 뿌리를 두고 있었다. 이 두 나무는 그 이후 계속 수렴해왔다. Lisp는 처음부터 강력했고, 다음 20년 동안 빨라졌다. 이른바 주류 언어들은 처음에는 빨랐고, 다음 40년 동안 점차 더 강력해져서, 이제 가장 발전한 것들은 Lisp에 꽤 가까워졌다. 가깝지만, 아직 몇 가지가 빠져 있다....
Lisp를 다르게 만든 것
처음 개발되었을 때 Lisp는 아홉 가지 새로운 아이디어를 구현하고 있었다. 그중 일부는 지금 우리가 당연하게 여기고, 일부는 더 발전한 언어들에서만 보이며, 두 가지는 아직도 Lisp만의 고유한 것이다. 이 아홉 가지 아이디어를 주류가 채택한 순서대로 나열하면 다음과 같다.
이 제한은 블록 구조 언어가 등장하면서 사라졌지만, 그때는 이미 늦었다. 식과 문장의 구분은 굳어져 있었다. 그것은 Fortran에서 Algol로, 그리고 다시 그 후손들 전체로 퍼져나갔다.
Lisp가 처음 등장했을 때, 이 아이디어들은 1950년대 후반의 하드웨어가 규정하던 일반적인 프로그래밍 관행과는 거리가 멀었다. 시간이 지나면서, 연이어 등장한 인기 언어들에 구현된 기본 언어는 점차 Lisp 쪽으로 진화해왔다. 아이디어 1-5는 이제 널리 퍼져 있다. 6번은 주류에 모습을 드러내기 시작했다. Python에는 7번의 일종이 있지만, 그것을 위한 문법은 없는 듯하다. 8번에 관해서는, 이것이 아마도 가장 흥미롭다. 8번과 9번은 McCarthy가 실제 구현될 것이라고는 전혀 생각하지 않았던 것을 Steve Russell이 구현했기 때문에 우연히 Lisp의 일부가 되었다. 그런데도 이 아이디어들이야말로 Lisp의 기묘한 외형과 가장 독특한 특징들의 원인이 되었다. Lisp가 이상하게 보이는 것은 이상한 문법을 가졌기 때문이라기보다 문법이 없기 때문이다. 다른 언어들이 파싱될 때 뒤에서 만들어지는 구문 트리 자체로 프로그램을 직접 표현하는데, 그 트리들이 리스트로 이루어져 있고, 리스트는 Lisp의 데이터 구조다.
언어를 자기 자신의 데이터 구조로 표현하는 것은 매우 강력한 특징으로 드러난다. 8번과 9번이 함께 있다는 것은 프로그램을 작성하는 프로그램을 쓸 수 있다는 뜻이다. 이상하게 들릴지 모르지만, Lisp에서는 일상적인 일이다. 그것을 하는 가장 흔한 방법은 _매크로_라고 부르는 것이다.
"매크로"라는 말은 Lisp에서 다른 언어에서와 같은 뜻이 아니다. Lisp 매크로는 약어일 수도 있고 새 언어를 위한 컴파일러일 수도 있다. Lisp를 정말 이해하고 싶거나 단지 프로그래밍의 시야를 넓히고 싶다면, 매크로에 대해 더 배워보라.
매크로는, 적어도 내가 아는 한, 아직도 Lisp에만 있는 독특한 기능이다. 부분적으로는 매크로를 가지려면 아마도 언어가 Lisp만큼 이상하게 보여야 하기 때문일 것이다. 또 한편으로는 그 마지막 한 단계의 힘까지 더하게 되면, 더 이상 새 언어를 발명했다고 주장할 수 없고, 단지 Lisp의 새 방언을 만든 것에 불과해지기 때문일 수도 있다.
이건 대체로 농담으로 하는 말이지만, 꽤 사실이다. car, cdr, cons, quote, cond, atom, eq, 그리고 리스트로 표현된 함수를 위한 표기법을 가진 언어를 정의한다면, 그 위에 Lisp의 나머지 전부를 구축할 수 있다. 실제로 이것이 Lisp를 정의하는 성질이다. McCarthy가 Lisp에 지금의 모양을 부여한 이유가 바로 이것이었다.
언어가 중요한 곳
그렇다면 Lisp가 주류 언어들이 점근적으로 접근하고 있는 어떤 한계 같은 것을 실제로 나타낸다고 하자. 그렇다고 정말 소프트웨어를 쓰는 데 Lisp를 써야 한다는 뜻일까? 덜 강력한 언어를 쓰면 얼마나 손해를 볼까? 때로는 혁신의 최전선에 서지 않는 편이 더 현명하지 않을까? 그리고 어느 정도까지는 인기도 그 자체로 정당화되지 않는가? 예를 들어 뾰족한 머리카락의 상사가 쉽게 프로그래머를 고용할 수 있는 언어를 쓰고 싶어 하는 것은 맞지 않은가?
물론 프로그래밍 언어의 선택이 그다지 중요하지 않은 프로젝트도 있다. 일반적으로 애플리케이션이 까다로울수록 강력한 언어에서 더 큰 지렛대를 얻는다. 하지만 전혀 까다롭지 않은 프로젝트도 많다. 대부분의 프로그래밍은 아마도 작은 접착용 프로그램을 쓰는 일로 이루어져 있을 것이고, 그런 작은 프로그램에는 이미 익숙하고 필요한 일을 할 라이브러리가 잘 갖춰진 어떤 언어든 쓸 수 있다. 한 Windows 앱에서 다른 앱으로 데이터를 넘기기만 하면 된다면, 물론 Visual Basic을 써도 된다.
Lisp로도 작은 접착용 프로그램을 쓸 수 있다. (나는 그것을 데스크톱 계산기로 쓴다.) 하지만 Lisp 같은 언어의 가장 큰 이점은 스펙트럼의 반대편, 치열한 경쟁 속에서 어려운 문제를 해결하기 위해 정교한 프로그램을 써야 하는 곳에서 나온다. 좋은 예는 ITA Software가 Orbitz에 라이선스하는 항공 요금 검색 프로그램이다. 이들은 이미 Travelocity와 Expedia라는 두 개의 크고 뿌리 깊은 경쟁자가 지배하고 있던 시장에 들어갔고, 기술적으로 그들을 완전히 압도한 것으로 보인다.
ITA 애플리케이션의 핵심은 20만 줄짜리 Common Lisp 프로그램으로, 경쟁자들보다 수많은 자릿수만큼 더 많은 가능성을 탐색한다. 경쟁자들은 겉보기에는 여전히 메인프레임 시대의 프로그래밍 기법을 사용하고 있다. (물론 어떤 의미에서는 ITA도 메인프레임 시대의 프로그래밍 언어를 사용하고 있다.) 나는 ITA의 코드를 본 적은 없지만, 그들의 최고 해커 중 한 명에 따르면 그들은 매크로를 많이 사용한다고 한다. 나는 그 말을 듣고도 전혀 놀랍지 않았다.
구심력
나는 흔치 않은 기술을 쓰는 데 비용이 전혀 없다고 말하는 것이 아니다. 뾰족한 머리카락의 상사가 이것을 걱정하는 것이 완전히 틀린 것은 아니다. 하지만 그는 위험을 이해하지 못하기 때문에 그것을 과장하는 경향이 있다.
덜 흔한 언어를 사용할 때 생길 수 있는 문제를 세 가지 떠올릴 수 있다. 당신의 프로그램이 다른 언어로 쓰인 프로그램과 잘 동작하지 않을 수 있다. 쓸 수 있는 라이브러리가 적을 수 있다. 그리고 프로그래머를 고용하기 어려울 수 있다.
이 각각은 얼마나 큰 문제일까? 첫 번째의 중요성은 시스템 전체를 통제할 수 있는지에 따라 다르다. 버그투성이의 폐쇄적인 운영체제 위에서 원격 사용자의 기계에서 돌아가야 하는 소프트웨어를 쓴다면, 애플리케이션을 OS와 같은 언어로 작성하는 데 장점이 있을 수 있다. (이름은 말하지 않겠다.) 하지만 ITA가 아마 그랬을 것처럼 시스템 전체를 통제하고 모든 부분의 소스 코드를 갖고 있다면, 원하는 어떤 언어든 쓸 수 있다. 호환성 문제가 생기면 직접 고치면 된다.
서버 기반 애플리케이션에서는 가장 진보한 기술을 사용해도 무방하다. 그리고 이것이 Jonathan Erickson이 말한 "프로그래밍 언어 르네상스"의 주된 원인이라고 생각한다. 우리가 Perl이나 Python 같은 새 언어들에 대해 듣는 이유가 바로 이것이다. 사람들이 이 언어들로 Windows 앱을 쓰기 때문이 아니라, 서버에서 그것들을 사용하기 때문이다. 그리고 소프트웨어가 데스크톱 밖으로 옮겨가 서버 위로 올라갈수록, (Microsoft조차 결국 그렇게 될 미래를 받아들이는 듯하다) 중간 수준의 기술을 써야 한다는 압력은 점점 줄어들 것이다.
라이브러리에 관해서도, 그 중요성은 애플리케이션에 따라 다르다. 덜 까다로운 문제에서는 라이브러리의 가용성이 언어 자체의 힘보다 중요할 수 있다. 손익분기점이 어디냐고? 정확히 말하기는 어렵지만, 그것이 어디든 여러분이 보통 애플리케이션이라고 부를 만한 것보다는 아래에 있다. 어떤 회사가 스스로를 소프트웨어 기업이라고 생각하고, 자신들의 제품 중 하나가 될 애플리케이션을 쓰고 있다면, 그 작업에는 아마 여러 해커가 참여하고 적어도 6개월은 걸릴 것이다. 그 정도 규모의 프로젝트에서는 강력한 언어가 이미 존재하는 라이브러리의 편리함을 능가하기 시작할 것이다.
뾰족한 머리카락의 상사의 세 번째 걱정, 즉 프로그래머를 고용하기 어렵다는 것은 내가 보기에는 논점 흐리기다. 어차피 몇 명의 해커를 고용해야 하는가? 소프트웨어는 열 명 미만의 팀이 가장 잘 개발한다는 사실을 이제 우리 모두 알고 있지 않은가. 그리고 그 정도 규모의 해커를 구하는 데는, 누구나 들어본 적 있는 어떤 언어라도, 큰 문제는 없어야 한다. Lisp 해커 열 명도 못 찾는다면, 당신의 회사는 아마 소프트웨어를 개발하기에 잘못된 도시에 있는 것이다.
사실 더 강력한 언어를 선택하면 필요한 팀 규모가 줄어들 가능성이 크다. 왜냐하면 (a) 더 강력한 언어를 쓰면 해커가 덜 필요할 가능성이 크고, (b) 더 발전한 언어로 일하는 해커들은 더 똑똑할 가능성이 높기 때문이다.
나는 "표준" 기술로 여겨지는 것을 쓰라는 압력을 많이 받지 않는다고 말하는 것이 아니다. Viaweb(지금의 Yahoo Store)에서 우리는 Lisp를 사용했다는 이유로 벤처 투자자들과 잠재 인수자들의 눈썹을 조금 치켜올리게 했다. 하지만 우리는 서버로 Sun 같은 "산업용급" 서버 대신 범용 Intel 박스를 사용한 일, 진짜 상용 OS인 Windows NT 대신 당시에는 거의 알려지지 않았던 오픈소스 Unix 변종 FreeBSD를 사용한 일, 지금은 아무도 기억하지 못하는 SET이라는 이커머스 표준을 무시한 일 등으로도 눈썹을 치켜올리게 했다.
양복 입은 사람들이 기술적 결정을 대신하게 해서는 안 된다. 우리가 Lisp를 쓴 것이 일부 잠재 인수자들을 불안하게 했는가? 조금은 그랬다. 하지만 우리가 Lisp를 쓰지 않았다면, 그들이 우리를 사고 싶어 하게 만든 소프트웨어를 쓸 수조차 없었을 것이다. 그들에게는 이상 현상처럼 보였던 것이 사실은 원인과 결과였다.
스타트업을 시작한다면, 벤처 투자자나 잠재 인수자를 만족시키기 위해 제품을 설계하지 마라. 사용자를 만족시키기 위해 제품을 설계하라. 사용자를 얻으면 나머지는 따라온다. 그리고 사용자를 얻지 못한다면, 당신의 기술 선택이 얼마나 안심되는 정통파였는지는 아무도 신경 쓰지 않을 것이다.
평균의 비용
덜 강력한 언어를 쓰면 얼마나 손해를 볼까? 사실 이에 관한 데이터가 조금 있다.
힘을 재는 가장 편리한 척도는 아마 코드 크기일 것이다. 고수준 언어의 요점은 더 큰 추상화, 말하자면 더 큰 벽돌을 제공해서, 같은 크기의 벽을 쌓는 데 벽돌이 덜 필요하게 하는 것이다. 그래서 언어가 더 강력할수록 프로그램은 더 짧아진다. (물론 단순히 문자 수 기준이 아니라, 구별되는 요소 기준에서다.)
더 강력한 언어는 어떻게 더 짧은 프로그램을 쓰게 해줄까? 그 언어가 허락한다면 사용할 수 있는 한 가지 기법은 상향식 프로그래밍이다. 단순히 기초 언어로 애플리케이션을 쓰는 대신, 그 기초 언어 위에 당신 같은 프로그램을 쓰기 위한 언어를 하나 만들고, 그 언어로 프로그램을 쓴다. 결합된 코드는 전체 프로그램을 기초 언어로 썼을 때보다 훨씬 짧아질 수 있다. 실제로 대부분의 압축 알고리즘이 바로 이런 식으로 동작한다. 상향식 프로그램은 수정도 더 쉬워야 하는데, 많은 경우 언어 계층은 전혀 바뀌지 않아도 되기 때문이다.
코드 크기가 중요한 이유는, 프로그램을 작성하는 데 걸리는 시간이 대부분 길이에 좌우되기 때문이다. 어떤 프로그램이 다른 언어로 쓰면 세 배 길어질 것이라면, 쓰는 데도 세 배 더 오래 걸릴 것이다. 그리고 더 많은 사람을 고용해서 이 문제를 피할 수는 없다. 왜냐하면 일정 규모를 넘어서면 새로 고용한 인력은 오히려 순손실이 되기 때문이다. Fred Brooks는 유명한 책 _The Mythical Man-Month_에서 이 현상을 설명했고, 내가 본 모든 것은 그의 말이 맞다는 쪽으로 기울었다.
그렇다면 Lisp로 쓰면 프로그램은 얼마나 더 짧아질까? 예를 들어 Lisp와 C를 비교한 수치로 내가 들어본 대부분은 7배에서 10배 정도였다. 하지만 최근 New Architect 잡지의 ITA 관련 기사에서는 "Lisp 한 줄이 C 20줄을 대체할 수 있다"고 했다. 그리고 그 기사에는 ITA 사장의 인용문이 가득했으므로, 이 수치도 ITA에서 나온 것이라고 추정한다. 그렇다면 그것을 어느 정도 믿어도 된다. ITA의 소프트웨어에는 Lisp뿐 아니라 C와 C++도 많이 포함되어 있으니, 그들은 경험에서 말하는 셈이다.
내 추측으로는 이런 배수는 상수가 아니다. 더 어려운 문제를 마주할수록, 그리고 더 똑똑한 프로그래머를 가질수록 커진다. 정말 뛰어난 해커는 더 좋은 도구에서 더 많은 것을 짜낼 수 있다.
어쨌든 곡선 위의 한 데이터 점으로서, 만약 ITA와 경쟁하려고 하면서 소프트웨어를 C로 쓰기로 했다면, ITA는 당신보다 소프트웨어를 20배 빠르게 개발할 수 있을 것이다. 당신이 새 기능 하나에 1년을 쓴다면, 그들은 그것을 3주도 안 되어 복제할 수 있다. 반대로 그들이 단 3개월을 들여 새 기능을 개발한다면, 당신이 그것을 갖게 되기까지는 _5년_이 걸릴 것이다.
그런데 말이다. 그것이 최선의 경우다. 코드 크기 비율에 대해 이야기할 때, 당신은 암묵적으로 그 프로그램을 더 약한 언어로도 실제 작성할 수 있다고 가정하고 있다. 하지만 실제로는 프로그래머가 할 수 있는 일에는 한계가 있다. 너무 저수준인 언어로 어려운 문제를 풀려 하면, 어느 순간 한 번에 머릿속에 담아둘 것이 너무 많아지는 지점에 도달한다.
그래서 내가 ITA의 가상의 경쟁자가 ITA가 Lisp로 3개월 만에 쓸 수 있는 것을 복제하는 데 5년이 걸릴 것이라고 말할 때, 그것은 아무 문제도 생기지 않을 경우의 5년을 뜻한다. 실제로 대부분의 회사에서 일이 돌아가는 방식을 보면, 5년이 걸리는 개발 프로젝트는 아예 끝나지 않을 가능성이 크다.
이것이 극단적인 사례라는 점은 인정한다. ITA의 해커들은 유난히 똑똑해 보이고, C는 꽤 저수준 언어다. 하지만 경쟁 시장에서는 두세 배 차이만 나도 당신이 항상 뒤처지게 되도록 보장하기에 충분하다.
레시피
이런 가능성은 뾰족한 머리카락의 상사가 생각조차 하기 싫어하는 종류의 것이다. 그래서 대부분은 생각하지 않는다. 결국 뾰족한 머리카락의 상사는 자기 회사가 된통 당하는 것은 크게 신경 쓰지 않는다. 다만 그게 자기 책임이라고 입증될 수만 없다면 말이다. 개인적으로 가장 안전한 계획은 무리의 중심 가까이에 붙어 있는 것이다.
큰 조직 안에서 이런 접근을 설명하는 데 쓰이는 말이 "industry best practice"다. 그 목적은 뾰족한 머리카락의 상사를 책임으로부터 보호하는 것이다. 그가 "industry best practice"를 선택했고 회사가 졌다면, 그를 비난할 수 없다. 선택한 것은 그가 아니라 업계였으니까.
내 생각에 이 용어는 원래 회계 방식 등을 설명하는 데 사용되었다. 그것이 대략 의미하는 바는 _이상한 짓을 하지 말라_는 것이다. 그리고 회계에서는 그것이 아마 좋은 생각일 것이다. "최첨단"과 "회계"는 잘 어울리는 말이 아니다. 하지만 이 기준을 기술 결정에 들여오면, 틀린 답을 얻게 되기 시작한다.
기술은 종종 최첨단이어야 한다. 프로그래밍 언어에서는, Erann Gat이 지적했듯이, "industry best practice"가 실제로 가져다주는 것은 최선이 아니라 단지 평균이다. 당신이 더 공격적인 경쟁자들보다 훨씬 느린 속도로 소프트웨어를 개발하게 만드는 결정이라면, "best practice"라는 말은 잘못된 이름이다.
여기에는 내가 매우 가치 있다고 생각하는 두 가지 정보가 있다. 사실 내 경험으로도 안다. 첫째, 언어마다 힘의 차이가 있다. 둘째, 대부분의 관리자들은 이것을 의도적으로 무시한다. 이 두 사실을 합치면, 말 그대로 돈 버는 레시피가 된다. ITA는 이 레시피가 작동하는 예다. 소프트웨어 사업에서 이기고 싶다면, 찾을 수 있는 가장 어려운 문제를 맡고, 구할 수 있는 가장 강력한 언어를 사용하고, 경쟁자들의 뾰족한 머리카락의 상사들이 평균으로 회귀하기를 기다리기만 하면 된다.
부록: 힘
프로그래밍 언어들의 상대적 힘이란 내가 무슨 뜻으로 말하는지 보여주는 예로, 다음 문제를 생각해보자. 우리는 누산기 생성 함수를 쓰고 싶다. 즉 숫자 n을 받아 또 다른 숫자 i를 받는 함수를 반환하고, 그 함수는 n에 i를 누적 증가시킨 값을 반환해야 한다.
(_plus_가 아니라 _incremented by_다. 누산기는 누적해야 하기 때문이다.)
Common Lisp에서는 이것이 다음과 같다.
(defun foo (n) (lambda (i) (incf n i))) 그리고 Perl 5에서는 sub foo { my ($n) = @_; sub {$n += shift} } 가 된다. Perl에서는 매개변수를 수동으로 꺼내야 하므로 Lisp 버전보다 요소가 더 많다.
Smalltalk에서는 코드는 Lisp보다 약간 더 길다.
foo: n |s| s := n. ^[:i| s := s+i. ] 왜냐하면 일반적으로 렉시컬 변수는 동작하지만, 매개변수에 대입은 할 수 없어서 새 변수 s를 만들어야 하기 때문이다.
Javascript 예제도 다시 약간 더 길다. Javascript는 여전히 문장과 식의 구분을 유지하기 때문에, 값을 반환하려면 명시적인
return 문이 필요하다: function foo(n) { return function (i) { return n += i } } (공정하게 말하면, Perl도 이 구분을 유지하지만, 전형적인 Perl 방식으로 return을 생략할 수 있게 해서 처리한다.)
Lisp/Perl/Smalltalk/Javascript 코드를 Python으로 번역하려고 하면 몇 가지 제약에 부딪힌다. Python은 렉시컬 변수를 완전히 지원하지 않기 때문에, n의 값을 담아둘 데이터 구조를 만들어야 한다. 그리고 Python에는 함수 데이터 타입이 있기는 하지만, 그것의 리터럴 표현은 없다. (본문이 단일 식 하나뿐인 경우를 제외하면.) 그래서 반환할 이름 있는 함수를 만들어야 한다. 결국 다음과 같은 코드가 된다.
def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar Python 사용자들은 왜 그냥 다음처럼 쓸 수 없는지 정당하게 물을 수 있다. def foo(n): return lambda i: return n += i 혹은 심지어 def foo(n): lambda i: n += i 그리고 내 짐작으로는 아마 언젠가는 그렇게 될 것이다. (하지만 Python이 남은 길을 다 가서 Lisp로 진화하기를 기다리고 싶지 않다면, 그냥...)
객체지향 언어들에서는 어느 정도까지는, 둘러싼 스코프에서 정의된 변수를 참조하는 함수인 클로저를, 메서드 하나와 둘러싼 스코프의 각 변수를 대체하는 필드를 가진 클래스를 정의함으로써 흉내 낼 수 있다. 이것은 렉시컬 스코프를 완전히 지원하는 언어에서라면 컴파일러가 해줄 코드 분석을 프로그래머가 직접 하게 만드는 것이고, 둘 이상의 함수가 같은 변수를 참조하면 동작하지 않지만, 이런 단순한 경우에는 충분하다.
Python 전문가들은 이것이 Python에서 이 문제를 푸는 선호되는 방법이라고 보는 듯하다. 다음 중 하나를 쓰는 방식이다.
def foo(n): class acc: def __init__(self, s): self.s = s def inc(self, i): self.s += i return self.s return acc(n).inc 또는 class foo: def __init__(self, n): self.n = n def __call__(self, i): self.n += i return self.n 이것들을 포함한 이유는 Python 옹호자들이 내가 언어를 잘못 묘사했다고 말하지 못하게 하기 위해서다. 하지만 내게는 둘 다 첫 번째 버전보다 더 복잡해 보인다. 하는 일은 똑같다. 누산기를 담아둘 별도의 장소를 마련하는 것이다. 단지 리스트의 머리 대신 객체의 필드를 쓰는 것뿐이다. 그리고 이런 특별한 예약 필드 이름들, 특히 __call__ 의 사용은 약간 임시방편처럼 보인다.
Perl과 Python의 경쟁에서 Python 해커들의 주장은 Python이 Perl보다 더 우아한 대안이라는 것인 듯하다. 하지만 이 사례가 보여주는 것은 힘이야말로 궁극의 우아함이라는 점이다. Perl 프로그램이 더 단순하다. (요소가 더 적다.) 문법이 약간 더 못생겼을지라도 말이다.
다른 언어들은 어떨까? 이 강연에서 언급된 다른 언어들, 즉 Fortran, C, C++, Java, Visual Basic에서는 이 문제를 실제로 해결할 수 있는지 분명하지 않다. Ken Anderson은 Java에서 다음 코드가 거의 가능한 최선이라고 말한다.
public interface Inttoint { public int call(int i); } public static Inttoint foo(final int n) { return new Inttoint() { int s = n; public int call(int i) { s = s + i; return s; }}; } 이것은 정수에서만 동작하므로 명세에 미치지 못한다. Java 해커들과 여러 차례 이메일을 주고받은 뒤의 내 판단으로는, 앞선 예제들처럼 동작하는 제대로 다형적인 버전을 작성하는 일은 끔찍하게 어색한 것과 불가능한 것의 중간 어디쯤이다. 누가 하나 써보고 싶다면 나는 매우 보고 싶겠지만, 개인적으로는 시간을 다 써버렸다.
다른 언어들에서는 이 문제를 해결할 수 없다고 말하는 것이 문자 그대로 사실은 물론 아니다. 이 모든 언어가 튜링 동등하다는 사실은, 엄밀히 말해 어떤 프로그램이든 어떤 언어로든 쓸 수 있다는 뜻이다. 그렇다면 어떻게 하겠는가? 극한의 경우에는, 덜 강력한 언어 안에 Lisp 인터프리터를 쓰면 된다.
농담처럼 들리겠지만, 대규모 프로그래밍 프로젝트에서는 이런 일이 정도의 차이는 있어도 너무 자주 일어나서, 이 현상에는 이름까지 있다. Greenspun의 열 번째 규칙이다.
충분히 복잡한 C 또는 Fortran 프로그램은 모두, Common Lisp의 절반을 임시변통으로 비공식 명세에 따라 버그투성이이면서 느리게 구현한 것을 포함하고 있다.
어려운 문제를 풀려고 한다면, 질문은 충분히 강력한 언어를 쓸 것인가가 아니다. 질문은 (a) 강력한 언어를 쓸 것인가, (b) 사실상 그런 언어의 인터프리터를 직접 쓸 것인가, 아니면 (c) 스스로 그런 언어의 인간 컴파일러가 될 것인가다. 우리는 이것이 Python 예제에서 이미 시작되는 것을 본다. 거기서는 사실상 렉시컬 변수를 구현하기 위해 컴파일러가 생성했을 코드를 우리가 시뮬레이션하고 있다. 이 관행은 흔할 뿐 아니라 제도화되어 있다. 예를 들어 객체지향 세계에서는 "패턴" 이야기를 많이 듣는다. 나는 이 패턴들이 때로는 (c), 즉 인간 컴파일러가 작동하고 있다는 증거가 아닌지 궁금하다. 나는 내 프로그램에서 패턴을 보면 문제의 신호로 여긴다. 프로그램의 형태는 오직 해결해야 할 문제만 반영해야 한다. 코드 속의 다른 규칙성은, 적어도 내게는, 내가 충분히 강력하지 않은 추상화를 쓰고 있다는 신호다. 흔히 그것은 내가 작성해야 할 어떤 매크로의 확장을 손으로 생성하고 있다는 뜻이다.
주
관련:
많은 사람들이 이 강연에 응답했기 때문에, 그들이 제기한 문제들을 다루기 위한 별도의 페이지를 만들었다: Re: Revenge of the Nerds.
또한 이것은 LL1 메일링 리스트에서 광범위하고 종종 유익한 토론을 촉발했다. 특히 의미 압축에 관한 Anton van Straaten의 메일을 보라.
LL1의 일부 메일은 언어의 힘이라는 주제를 더 깊이 파보도록 이끌었고, 그 결과가 Succinctness is Power이다.
누산기 생성기 벤치마크의 더 큰 표준 구현 모음은 별도 페이지에 함께 정리되어 있다.
Japanese Translation, Spanish Translation, Chinese Translation