주의: 이 글은 컴퓨터 세계의 세 종교(운영체제, 프로그래밍 언어, 에디터) 중 하나에 대해 이야기한다. 이들의 선택은 어디까지나 선호의 문제이며 이곳에 적힌 내 생각은 많은 오류를 포함한다.
최근 매크로에 관심이 생겨 Lisp을 보다가 조금 당연한 결론에 도달했다.
매크로는 역시나 나쁘다.
매크로가 없는 언어는 거의 없다. C나 C++같은 언어는 문자열 치환으로 이를 구현했고 OCaml은 AST 수준 매크로를 도입했다. Lisp 계열 언어는 S-expression 문법의 단순함 덕분에 더욱 강력한 매크로 기능을 지원한다.
이들 매크로를 이용하면 그 기능에는 약간씩 차이가 있지만 결국 하고자 하는 것은 같다. 주어진 언어 문법으로 예쁘게 표현하기 어려운 부분을 매크로로 표현하는 것이다. 예를 들면 C99 이전 C에서는 함수 inline
을 흉내내기 위해 매크로를 썼다. OCaml에서는 모든 타입에서 일반적으로 동작하는 compare
함수 정의에 매크로를 쓴다. 가진 것이 오직 리스트와 함수 호출밖에 없는 Lisp에서는 다른 문법 구조들을 추가할 때 매크로를 쓴다. 결국 하고자 하는 것은 언어의 '자유로운' 확장이다.
언어의 확장이 자유롭다니 왠지 멋지다. 😎 이것이야말로 스스로 진화하는 컴퓨팅(AI)의 뿌리 아닌가!
하지만 나는 이것이 매크로의 가장 큰 문제라고 생각한다. 정확히는 언어의 '동적 확장'에 반대한다.
제 멋대로 확장된 언어로 짜여진 프로그램은 읽기 어렵다.
보통 프로그램: 언어의 문법은 고정되어 있고 대강 다 알고 있다. 그래서 프로그램을 읽을 때 프로그램이 하는 일에만 집중한다.
매크로가 사용된 프로그램: 하지만 이젠 다르다. 매크로에 의해 언어가 확장됐기 때문에 프로그램의 하는 일을 이해하기 위해선 먼저 확장된 언어를 먼저 이해해야 한다.
예를 들어 (foo e1 e2)
와 같은 Lisp 코드가 있었다고 하자. 매크로가 없다면 먼저 e1
이 계산될 것이고 그 다음 e2
가 계산될 것이고 그 다음 함수 foo
가 호출될 것이다. 이제 알아야 하는 건 e1
, e2
가 무엇을 계산하는지, foo
는 어떤 일을 하는지이다. 우리는 문법을 이미 알고 있기 때문에 프로그램이 정말 무슨 일을 하는지만 집중해서 알면 된다.
매크로가 있다면 상황은 복잡해진다. e1
이 계산되지 않은 채로 foo
의 내용물이 실행될지, 그 후에 e1
이 두 번 계산될지, 혹은 e1
보다 e2
가 먼저 실행될지가 foo
가 어떤 매크로냐에 따라 결정된다. 프로그램이 하는 일을 이해하기에 앞서 foo
라는 새로운 문법을 알아야 한다. foo
는 더 이상 함수 이름이 아니다. 심지어 Lisp에선 이런 확장이 실행 도중에 이루어질 수도 있다. 비관적으로 말해서 프로그램이 하는 일을 이해하기 위해 새로운 문법을 알아야 하는데 새로운 문법을 알기 위해 프로그램이 하는 일을 알아야 하는 것이다. 망했다. 문자열 치환으로 매크로를 구현하거나 AST 수준 매크로를 사용하는 다른 언어에서도 상황이 다르지 않다. Lisp과 달리 매크로 기능의 근본 없음 때문에 어쩌면 더 나쁠지도 모른다.
자유롭게 언어를 확장하며 프로그래밍을 하는 사람은 그 'hacky한' 아름다움에 감동할지도 모르지만 다른 사람은 그 코드를 읽기 위해 새로운 언어를 배워야 한다. 그것이 표현력 부족한 언어를 위해 매크로가 제공하는 것이니까. 종종 Lisp을 보고 간단한 언어라 부르는 이유는 단지 프로그램 작성이나 실행 전에 필요한 모든 문법이 아직 정의되어 있지 않았기 때문이지 정말 리스트와 함수 호출만으로 문법 확장 없이 모든 걸 간결하게 표현할 수 있기 때문이 아니다.
내 마음 속 일 번은 가독성이다. 아이러니하게도 매크로가 등장한 배경도 가독성이다. 두둥! 언어의 문법적 한계에서 오는 코드 중복을 피하려고 문법을 확장할 수 있도록 한 것이다. 문법 확장에서 오는 다른 의미에서의 가독성 저하는 고려하지 않은 채로 말이다.
그 대가가 문법 확장이라면 난 차라리 코드를 조금 더 중복시키고 더 길게 작성하는 편을 택할 것이다. 수개월, 수년 후에 내가 또 읽을지도 모르는데 그때 메뉴얼도 없는 이상한 언어를 새로 배우고 싶지는 않다.
유명한 해커 Paul Graham이 Arc의 튜토리얼에서 이런 말을 했다.
I wouldn't be surprised if some parts of my code go through 10 or 20 levels of macroexpansion before the compiler sees them, but I don't know, because I've never had to look.
자신이 작성한 매크로 코드를 한 번도 볼 필요가 없었다니. 그것도 타입도 없는 언어에서. 버그가 없었단 말인가, 디버깅을 안했단 말인가. 역시 대단한 해커다. 나도 그 정도라면 Lisp을 안 쓸 이유가 없다. (아니 사실은 있다.) 그치만 난 내가 쓴 쓰레기같은 코드를 보고 고치고 보고 고치고, 울면서 반복한다.
2017-02-23 씀.