다음에 올 것을 만들어가는 기술에 대한 심층 기사.

60년이 지나도 Lisp이 중요한 이유

65년이 넘은 Lisp이 여전히 현대 언어 설계에 영향을 주는 이유와, 오늘날 개발자가 실제로 배울 수 있는 것을 정리했습니다.

여러 겹의 괄호 모양 가지에 빛나는 열매가 달린 고대 나무

몇 년에 한 번씩 'Lisp은 죽었다'는 글이 올라오고, 그때마다 누군가는 여러분이 즐겨 쓰는 현대 언어의 기능 절반이 Lisp에서 가져온 것이라고 지적합니다. 가비지 컬렉션, 일급 함수, 클로저, 동적 타이핑, 호모이코니시티, REPL 기반 개발, 매크로 모두 오늘날 대부분의 개발자가 태어나기 전에 Lisp에서 발명되거나 대중화된 것들입니다. Lisp은 그 가치를 온전히 이해하는 데 수십 년이 걸리는 종류의 프로젝트입니다.

그런데도 현업 개발자에게 Lisp을 써봤냐고 물으면 대부분 아니라고 답합니다. 실서비스 프로젝트를 Lisp으로 시작할 거냐고 물으면 다들 이상하다는 듯이 쳐다보죠. 여기에는 역설이 있습니다. Lisp의 아이디어는 프로그래밍 세계를 정복했지만, Lisp 자체는 여전히 틈새 언어에 머물러 있다는 것입니다. 이유를 이해하는 것은 대부분의 Lisp 옹호글에서 볼 수 있는 'Lisp은 좋고 다른 언어는 나쁘다' 식의 주장보다 훨씬 흥미롭습니다.

Lisp이 실제로 제대로 짚었던 것들

Lisp은 1958년 John McCarthy가 만들었습니다. 감을 잡자면, 당시 FORTRAN은 이제 막 1년이 된 상태였고 COBOL은 아직 존재하지도 않았습니다. PDP-1 미니컴퓨터가 출시되기까지도 2년이 더 남아 있었죠. Lisp은 종이 위에서 설계되었고, 방 하나를 가득 채우는 36비트 워드 머신인 IBM 704에서 구현되었습니다.

그럼에도 McCarthy가 내린 설계 결정들은 60년이 지난 지금도 유효합니다. 대표적인 것들은 다음과 같습니다:

코드가 곧 데이터 (호모이코니시티)

대부분의 언어에서 코드와 데이터는 근본적으로 다른 것입니다. 특정 문법으로 코드를 작성하면, 그 코드가 자료구조를 조작하죠. Lisp에서는 코드 자체가 데이터입니다. Lisp 프로그램은 리스트로 이루어져 있습니다. (+ 1 2)라는 식은 1과 2를 더하는 함수 호출인 동시에, 심볼 +, 숫자 1, 숫자 2라는 세 개의 원소를 가진 리스트이기도 합니다. 이 리스트를 재배열하거나, 원소를 추가하거나, 변환한 뒤 그 결과를 실행할 수 있습니다.

;; This is a function call
(+ 1 2)  ; => 3
;; This is a list containing the same elements
'(+ 1 2)  ; => (+ 1 2)
;; You can build code as data and then evaluate it
(def my-expr '(+ 1 2))
(eval my-expr)  ; => 3
;; Or transform it
(def doubled (list '* 2 my-expr))
;; doubled is now (* 2 (+ 1 2))
(eval doubled)  ; => 6

이건 단순한 재미 요소처럼 보이지만, 프로그램이 프로그램을 만들어낸다는 것을 가능하게 한다는 점을 깨닫는 순간 달라집니다. Lisp 매크로는 C 전처리기 매크로처럼 텍스트를 치환하지 않습니다. 실제 프로그램 구조를 자료구조로 받아 언어의 모든 기능을 사용해 변환하고, 새로운 프로그램 구조를 반환합니다. 컴파일 시점의 코드 생성이며, 컴파일러 자체가 API가 되는 셈입니다.

개발 환경으로서의 REPL

Lisp은 Read-Eval-Print Loop, 즉 REPL을 처음으로 선보였습니다. 식을 입력하면 즉시 평가되고 결과가 보이는 방식이죠. 모든 언어가 REPL을 갖춘 지금은 평범하게 느껴질 수 있지만, Lisp의 REPL은 대부분의 것보다 한 걸음 더 나아갑니다.

Common Lisp이나 Clojure에서는 REPL에서 고립된 식만 테스트하지 않습니다. 시스템 전체를 대화형으로 개발하죠. 프로그램이 실행 중인 상태에서 함수를 정의하고, 테스트하고, 다시 정의합니다. 실행 중인 상태를 살펴보고 수정할 수도 있으며, 애플리케이션을 재시작하지 않고 함수 하나만 다시 컴파일할 수 있습니다. 개발 사이클은 작성-컴파일-실행-디버그가 아니라, 살아 있는 시스템과 끊임없이 주고받는 대화입니다.

특히 Clojure 개발자들은 이런 방식으로 일하는 경우가 많습니다. 에디터를 실행 중인 REPL에 연결하고, 에디터에서 코드를 작성한 뒤 키 한 번으로 개별 식을 평가해 결과를 즉시 확인합니다. 피드백 루프는 분 단위가 아니라 밀리초 단위로 측정됩니다. 이 방식에 익숙해지면, 전통적인 편집-컴파일-재시작 사이클은 편지로 대화하는 것처럼 느껴지게 됩니다.

최소한의 문법, 최대의 유연성

Lisp의 문법, 혹은 문법이 없다는 점은 가장 논쟁적인 특징입니다. if 문, for 루프, 클래스 정의를 위한 특수 형식이 따로 없습니다. 모든 것이 연산자를 맨 앞에 둔 리스트입니다: (if condition then-expr else-expr), (defn name [args] body), (for [x (range 10)] (* x x)). 초보자들이 거부감을 느끼는 괄호는, 지금까지 설계된 언어 중 가장 문법적으로 일관된 언어를 얻기 위해 치르는 대가입니다.

이런 일관성은 도구 지원에서 실질적인 이점을 줍니다. 모든 구문이 같은 (연산자 피연산자...) 패턴을 따르면, 에디터가 들여쓰기, 리팩터링, 구조 기반 탐색을 안정적으로 수행할 수 있습니다. '감싸고 있는 식 선택하기'는 항상 짝이 맞는 괄호가 기준이라 Lisp에서는 모호함이 없습니다. 문법이 다양한 언어에서는 구조적 편집이 아직 풀리지 않은 문제이지만, Lisp에서는 1960년대부터 해결되어 있었습니다.

Lisp이 승리하지 못한 이유

Lisp이 그렇게 훌륭하다면 왜 모두가 쓰지 않을까요? Lisp 옹호자들의 표준 답변은 '업계가 틀렸다'인데, 이는 도움이 되지 않을 뿐 아니라 대부분 사실이 아닙니다. Lisp이 주류가 되지 못한 데에는 실질적이고 사소하지 않은 이유가 있습니다.

  • 생태계 격차. 대부분의 실무에서는 언어 기능보다 라이브러리가 더 중요합니다. Python이 인기 있는 이유는 문법이 아름답기 때문이 아니라, pip install 한 줄로 데이터 과학, 웹 개발, 머신러닝 등 모든 분야에서 잘 관리되는 수천 개의 패키지를 바로 쓸 수 있기 때문입니다. Lisp 생태계(Common Lisp, Scheme, Clojure)도 탄탄하지만 규모가 작습니다. 직접 처음부터 구현하거나, 관리가 덜 된 라이브러리를 맞춰 쓰는 데 더 많은 시간을 쓰게 됩니다.
  • 학습 곡선이 초반에 몰려 있습니다. Lisp의 괄호 문법은 한번 익히면 놀랄 만큼 단순하지만, 초보자에게는 실질적인 장벽입니다. 더 중요한 것은, 관용적인 Lisp을 쓰려면 절차형이나 객체지향 언어에 익숙한 사람에게 낯선 방식으로 생각해야 한다는 점입니다. 보상은 나중에 오지만, 많은 개발자가 그 지점에 도달하기 전에 포기합니다.
  • 파편화. 'Lisp'은 하나의 언어가 아니라 언어 가족입니다. Common Lisp, Scheme, Racket, Clojure, Emacs Lisp, Hy, Janet 등 각각 강점, 생태계, 커뮤니티가 다릅니다. 이런 파편화 때문에 생태계를 키우는 데 필요한 노력이 한데 모이지 못합니다.
  • 기업의 후원이 중요합니다. Java에는 Sun이, C#에는 Microsoft가, Go에는 Google이 있었습니다. Python은 Google과 거대한 데이터 과학 커뮤니티의 지원을 받았죠. 실서비스에서 가장 성공한 Lisp 계열 언어인 Clojure는 Rich Hickey가 매우 명료하게 사고하고 소통하는 사람이라는 점 덕분에 주목받았지만, 주류 채택을 이끄는 제도적 추진력은 여전히 부족합니다.

Clojure: 역사에서 배운 Lisp

Clojure는 특별히 주목할 만합니다. Lisp의 강점을 현대 소프트웨어 개발에 실용적으로 적용하는 방법을 보여주기 때문입니다. Rich Hickey는 2007년 이전 Lisp들이 주류가 되지 못한 이유를 명확히 인식한 채 Clojure를 설계했고, 의도적인 절충을 선택했습니다.

  • JVM 위에서 실행됩니다. Clojure는 처음부터 별도의 생태계를 만드는 대신 Java Virtual Machine 위에서 실행되며, 모든 Java 라이브러리를 직접 사용할 수 있습니다. 데이터베이스 드라이버가 필요하면 Java 것을 쓰고, HTTP 클라이언트가 필요하면 Java 것을 씁니다. 이 한 번의 결정으로 Clojure는 출시 첫날부터 검증된 수천 개의 라이브러리를 쓸 수 있었습니다.
  • 기본적으로 불변입니다. Clojure의 핵심 자료구조인 리스트, 벡터, 맵, 집합은 모두 불변입니다. 맵을 수정하는 대신, 변경 사항을 반영한 새 맵을 만듭니다. 이를 통해 동시성 버그의 범주 전체를 없애고 프로그램을 더 쉽게 추론할 수 있습니다. 내부의 영속 자료구조는 구조를 공유해 이 방식을 효율적으로 만듭니다.
  • 실용적인 동시성 기본 요소. Atom, Ref, Agent처럼 Clojure는 서로 다른 패턴에 맞는 여러 동시성 모델을 제공합니다. 이는 이론적 우아함이 아니라, 실서비스 Java 애플리케이션에서 공유 가변 상태가 주는 고통을 Hickey가 직접 겪은 경험에서 나온 것입니다.
  • ClojureScript. Clojure는 JavaScript로 컴파일되어 브라우저와 Node.js 생태계를 활용할 수 있습니다. 백엔드는 Clojure로, 프론트엔드는 ClojureScript로 작성하고 둘 사이에서 코드를 공유할 수 있습니다. TypeScript나 Kotlin Multiplatform과 같은 약속이지만, 훨씬 먼저 등장했습니다.

현대 언어들이 빌려 쓴 것들

Lisp 코드를 한 줄도 쓰지 않더라도, 여러분은 매일 Lisp의 아이디어를 사용하고 있습니다. 그 영향력은 너무 광범위해서, 얼마나 깊이 스며들었는지 추적하는 것 자체가 하나의 탐구가 될 정도입니다.

JavaScript의 map, filter, reduce는 Lisp의 리스트 처리 함수를 직접 이어받은 것입니다. 언어 이름 자체가 그 기원을 말해줍니다(LISt Processing). Python의 리스트 컴프리헨션은 같은 연산을 문법적으로 편하게 감싼 것입니다. Rust의 패턴 매칭은 ML을 거쳐 Lisp의 cond 식까지 거슬러 올라갑니다. Swift의 클로저, Kotlin의 람다 문법, Java의 Streams API 모두 옷만 바꿔 입은 Lisp 개념들입니다.

Rust의 매크로 시스템(macro_rules!와 프로시저 매크로), Elixir, Nim, Julia의 매크로는 모두 Lisp 매크로에서 직접 영감을 받았습니다. 다만 이들 언어 중 호모이코니시티를 가진 언어가 없기 때문에, 같은 수준의 매끄러움은 어느 곳에서도 완전히 구현되지 못했습니다. 코드가 데이터이면 매크로는 간단해지지만, 코드의 문법이 복잡하면 그 문법을 파싱하고 생성해야 하므로 마찰이 생깁니다.

REPL 기반 개발은 이제 데이터 과학에서 표준이 되었습니다(Jupyter 노트북은 사실상 상태를 유지하는 REPL입니다). Figwheel과 shadow-cljs 같은 도구는 프론트엔드 개발에 핫 리로딩을 가져왔는데, 이는 실행 중인 시스템에 대고 개발한다는 Lisp 전통에서 곧장 나온 아이디어입니다.

Lisp을 배워야 할까?

정직한 답은 여러분이 무엇을 얻고 싶은지에 달려 있습니다.

코드를 바라보는 방식을 넓혀 더 나은 프로그래머가 되고 싶다면, 답은 분명 '그렇다'입니다. 특히 데이터 변환, 재귀 구조, 코드-as-데이터 관점에서 생각하는 법을 배우면, 어떤 언어로 일하든 문제에 접근하는 방식이 바뀝니다. 함수형 프로그래밍을 배우는 것과 비슷합니다. 실무에서 Haskell을 쓰지 않더라도, 그 개념 덕분에 Python과 JavaScript를 더 잘 쓰게 됩니다.

Lisp으로 실서비스 소프트웨어를 만들고 싶다면 Clojure가 현실적인 선택입니다. 실제로 쓸 만한 생태계와 전문 커뮤니티가 있고, Nubank, Walmart, CircleCI 같은 기업들이 대규모로 운영하고 있습니다. Common Lisp도 충분히 쓸 수 있지만 틈새에 가깝고, 실제 문제보다 인프라에 더 많은 시간을 쓰게 됩니다. Scheme과 Racket은 교육과 언어 연구에는 훌륭하지만 범용 개발에는 덜 실용적입니다.

관심은 있지만 아직 본격적으로 시작할 준비가 되지 않았다면, Clojure 코드를 읽어보세요. 튜토리얼이 아니라 실제 운영 코드를 보세요. Clojure 개발자들이 작은 함수들을 어떻게 조합하는지, 데이터 파이프라인을 만들 때 스레딩 매크로(->와 ->>)를 어떻게 쓰는지, 클래스 대신 평범한 맵으로 도메인을 어떻게 모델링하는지 살펴보세요. 주류 OOP 코드베이스에서 보던 것보다 더 간결하고, 조합이 쉬우며, 데이터 흐름에 더 집중하는 프로그래밍 스타일을 보게 될 겁니다.

Lisp의 생명력은 향수가 아닙니다. 몇 가지 근본적인 것을 워낙 제대로 잡았기 때문에, 60년간의 언어 설계도 그것을 넘어서지 못한 결과입니다. 괄호는 낯설고, 생태계는 여러분이 바라는 것보다 작습니다. 하지만 그 아이디어들은 시대를 타지 않으며, 그것들과 시간을 보내면 여러분이 매일 쓰는 어떤 언어로든 더 나은 코드를 쓰고 있을 겁니다.