프로그래밍 언어를 왜 만들고, 어떻게 설계하고, 어떻게 구현을 시작할지에 대한 조언과 참고 자료를 정리한다.
URL: https://thesephist.com/posts/pl/
Title: 나만의 프로그래밍 언어 만들기
내가 만든 프로젝트들 중에서, 가장 많은 질문을 받는 것은 Ink 프로그래밍 언어에 관한 것이다. 질문들은 대개 세 가지 범주로 나뉜다.
이 글은 (1)과 (2)의 질문에 답하고, 여러분이 자신만의 언어를 시작할 수 있도록 몇 가지 자료를 제공하는 것을 목표로 한다. (3)은 더 기술적인 별도의 글로 남겨두는 편이 좋다고 생각한다.
이 글은 다음 몇 개의 섹션으로 구성되어 있다.
새로운 프로그래밍 언어를 위한 인터프리터를 만드는 일은 내가 해본 프로젝트 중 기술적으로 가장 큰 보상을 준 작업이었다. 이 과정은 소프트웨어의 근본 개념에 대한 내 이해를 밀어붙였고, 컴퓨터가 어떻게 동작하는지 더 깊은 수준에서 이해하게 해주었다.
프로그래밍을 막 시작한 개발자에게 프로그래밍 언어와 컴파일러/인터프리터는 블랙박스일 수 있다. 우리는 그것을 당연한 도구로 받아들이며, 언어나 원시 도구들이 내부에서 어떻게 동작하는지 꼭 알지 못해도 좋은 소프트웨어를 만들 수 있다. 하지만 프로그래밍 언어를 만든다는 것은 컴퓨팅 스택의 가장 “살이 두꺼운” 부분을 이해하는 일이다. CPU와 메모리에서 시작해 운영체제, 메모리 내 데이터 구조, 프로세스, 스레드, 스택, 그리고 라이브러리와 애플리케이션까지 이어진다. 또한 이는 컴퓨팅에서 가장 흥미로운 수학으로 들어가는 수문이기도 하다. 범주론과 타입 시스템, 대수, 그래프 이론의 다양한 응용, 복잡도 분석 등. 프로그래밍 언어를 쓰고 공부하면서, 나는 컴퓨터가 동작하는 수학적·기계적 토대를 모두 더 익숙하게 이해하게 되었다.
그 결과, 내 일상 업무가 프로그래밍 언어 도구나 컴파일러 작성과는 거의 관련이 없더라도, 이 프로젝트는 나를 더 나은 개발자로 만들어주었다.
또한 프로그래밍 언어는 버전 1이 “완성”된 뒤에도 확장, 최적화, 추가적인 창의성을 발휘할 여지가 아주 많다. 초기 구현을 신중하게 설계하면, 새로운 키워드와 기능, 최적화, 도구를 언어와 인터프리터/컴파일러에 시간에 따라 점진적으로 추가할 수 있다. 언어는 장기 프로젝트이자 창작의 배출구가 되기 쉽고, 나는 버전 1을 만든 뒤 다른 언어들을 공부하면서 초기 구현을 확장하는 일을 즐겨왔다.
소프트웨어의 다른 영역들 중 일부가 유행 주기를 따르는 것과 달리, 프로그래밍 언어 분야는 학계와 산업계 모두에서 오랫동안 꾸준히 성장하고 깊어져 왔다. 새로운 언어에 결합될 수 있는 수많은 개념에 관한 문헌, 연구, 선행 사례, 실전 조언이 엄청나게 많고, 이를 구현하는 방식에도 수많은 변형이 존재한다.
특히 개인 프로젝트로 만드는 언어라면, 모든 것에 최고이고 모든 언어의 장점을 결합한 “유일무이한 언어”를 만들려 하기보다는, 컴퓨팅의 고고학에서 흥미로운 아이디어 한두 개를 골라 그 아이디어를 깊게 파고드는 프로그래밍 언어를 만드는 것이 가장 결실 있는 접근이라고 생각한다. 모든 사용 사례에 대해 그런 언어를 만드는 것은 불가능할 뿐 아니라, 그런 사고방식은 개발 속도를 늦추고 불필요한 일을 늘리며, 관심 있는 몇 가지 주제에 대해 많이 배울 수 있는 정신적 여유를 빼앗는다.
예를 들어, 함수형 프로그래밍과 불변 데이터 구조에 관심이 있을 수 있다. 그렇다면 모든 상태를 불변 데이터 구조에 저장하도록 설계된 언어를 만들고, 그런 데이터 구조가 컴파일러에서 어떻게 최적화되고 효율적으로 조작될 수 있는지 연구해볼 수 있다. 혹은 저수준 C 프로그래밍과 시스템 언어에 관심이 있다면, C API 및 바이너리 인터페이스와의 우아한 상호운용성을 주요 목표로 하는 스크립팅 언어를 만들어볼 수도 있다.
어디에서 시작하느냐에 따라 마주하게 될 도전과 트레이드오프도 달라진다. 예를 들어 Lisp 인터프리터는 문법이 단순해서 파싱이 쉬우므로 더 쉽게 작성할 수 있을지도 모른다(정적 타입 시스템을 가진 언어의 더 복잡한 문법과 비교하면). 하지만 Lisp 계열은 클로저 같은, 실제 실행되는 저수준 머신 코드에서 더 멀어진 의미론을 갖는 경우가 많다. 그래서 파싱의 용이함과 실행 복잡도 사이에서 트레이드오프가 생길 수 있다.
싸움을 신중하게 고르고, 깊게 공부하고 싶은 몇 가지 핵심 아이디어에 집중하라.
인터프리터나 컴파일러를 작성하기 전에, 먼저 언어를 설계 해야 한다. 범주론과 타입 시스템, 프로그래밍 패러다임을 공부하는 데 시간을 쓸 수도 있지만, 이 단계에서 스스로에게 가장 도움이 되는 것은 폭넓은 프로그래밍 언어들에서 아이디어를 공부하는 일이다. Rust나 Python 같은 “현대적” 언어에만 머무르지 말고 컴퓨팅 역사로 되돌아가 보라. 적어도 한 세기 전의 컴퓨팅과 수학까지 거슬러 올라가면, 흥미롭게 재발명하고 조합할 수 있는 풍부한 아이디어가 있다.
인기 있는 현대 OOP 유사 언어들(Python, Ruby, Swift, C++)의 정신 모델에 갇혀 이 단계에서 문법 설계에 대부분의 시간을 쓰기 쉽다. 하지만 언어의 “감”을 좌우하고, 여러분이 가장 많이 배울 수 있는 것은 의미론(semantics) 이므로, 여기에 더 많은 시간을 써야 한다. 의미론은 문법보다 사후에 바꾸기 더 어렵기도 하다. 언어의 의미론과 사용성을 생각하기 시작하기 위해 스스로에게 던져볼 질문들:
for, while 같은 네이티브 반복 제어 흐름 도구를 제공하는가?이 질문들에 답하기 위해서는, 세상에 있는 다양한 언어를 직접 맛보는 것이 가장 도움이 된다고 생각한다. 아래는 내가 추천하는 작은 입문용 목록과, 각 언어(또는 언어 계열)에서 내가 개인적으로 흥미롭다고 느낀 아이디어들이다.
언어가 어떻게 동작하고 느껴져야 하는지에 대한 설계가 생기면, 먼저 그 언어로 작성된 프로그램이 실제로 어떤 느낌인지 “시험 운전”해봐야 한다.
이 단계에서는 보통 텍스트 문서 하나를 열어두고, 아직 존재하지 않는 새 언어로 작은 프로그램을 써보며 문법을 실험한다. 코드 주석으로 스스로에게 메모와 질문을 남기고, 재귀 수학 함수, 정렬 알고리즘, 작은 웹 서버 같은 간단한 알고리즘과 자료구조를 구현해본다. 나는 보통 문법과 의미론에 대한 대부분의 질문이 해소될 때까지 이 단계에 머문다. 또한 이 단계는 언어 설계의 핵심 아이디어에 대한 가설을 테스트하는 데도 사용한다. 예를 들어 함수형 프로그래밍과 비동기 이벤트 기반 동시성을 중심으로 언어를 설계했다면, 이 단계에서 샘플 코드를 작성해 그 아이디어들이 잘 결합되는지, 내가 즐겁게 쓸 만한 프로그램으로 이어지는지 검증해볼 것이다. 예상이 틀렸다면, 가정을 업데이트하거나 언어 설계를 수정해 배운 내용을 반영한다.
이 허수아비(straw-man) 설계 단계는 구현을 많이 쓰지 않고도, 머릿속에서 발명한 언어가 여러분이 쓰고 싶은 프로그램에 잘 맞을지 빠르게 확인하는 방법이다.
핵심만 말하면, 인터프리터와 컴파일러는 소스 코드(텍스트 문자열)를 컴퓨터가 실행할 수 있는 어떤 형태로 바꾸기까지의, 작은 변환들의 층이 겹겹이 쌓인 것이다. 이를 설계하는 기술은 이런 변환들이 다룰 좋은 중간 표현을 고르고, 실행 중인 프로그램 안에서 언어의 값과 함수를 표현할 효율적인 런타임 데이터 구조를 선택하는 일로 요약된다.

똑똑한 사람들은 인터프리터와 컴파일러 설계를 평생 연구하고, 나는 블로그 글의 한 소절로 이 주제를 제대로 다룰 수 없다. 그래서 여기서는 헛된 시도를 하기보다, 취미 프로젝트에서 더 나은 인터프리터와 컴파일러를 구현하려는 내 시도에 가장 도움이 되었던 자료들로 안내하려 한다.
이 주제에 대한 내가 가장 좋아하는 자료 두 개는 전자책 두 권이다. Robert Nystrom의 Crafting Interpreters와 Ruslan Spivak의 Let’s Build A Simple Interpreter이다. 두 글/시리즈 모두 프로그래밍 언어에 완전 초보인 상태에서 시작해, 단순한 인터프리터를 단계별로 설계하고 만드는 과정을 안내한다. 이 책들이 인터프리팅의 모든 주제를 상세히 다루지는 않겠지만, 각 단계가 어떻게 동작하는지에 대한 출발점을 제공해, 첫 번째 동작하는 프로토타입까지 갈 수 있을 만큼은 알려준다.
많은 사람들이 이 가이드를 그대로 따라 책의 코드를 한 줄씩 따라 치며 인터프리터를 만들었다. 나는 그보다는 훑어본 뒤, 내 여정 전반에서 참고서처럼 각 장을 필요할 때 찾아보며 사용했다. 그럼에도 이 책들은, 그 위에 추가로 인터프리터와 컴파일러가 동작하는 방식의 다른 주제들을 공부할 수 있는 매우 탄탄한 토대를 제공한다.
읽어보진 않았지만, Writing an Interpreter in Go와 Writing a Compiler in Go 두 권의 책도 좋은 평가를 받는 것으로 알고 있다. Go 언어로, 프로그래밍 언어와 인터프리터/컴파일러를 처음부터 만드는 과정을 안내한다. 위에서 소개한 두 전자책처럼, 단순한 최소 증명(proof-of-concept)만 만드는 것이 아니라, 현실적인 언어를 위한 인터프리터나 컴파일러를 작성할 때 마주치는 기본기와 잠재적 함정, 고려사항을 폭넓게 다룬다.
아래부터는 필수 독서는 아니다 — 소개 수준부터 고급까지 다양하고, 미래의 나를 위해 목록을 모아 둔 면도 있다. 모든 항목이 다 흥미롭지 않아도 괜찮다.
그렇다면, 인터프리터와 컴파일러에 대한 추가 온라인 읽을거리 모음은 다음과 같다…
나는 훌륭한 기술 강연도 좋아한다. 수년간 유용하고 흥미롭게 본 몇 가지를 소개하면…
마지막으로, 잘 설계되고 문서화가 잘 된 인터프리터와 컴파일러의 구현을 읽는 동안에도 많은 것을 배웠다. 그중에서도 특히 즐겁게 본 것은:
여기 적은 목록은, 내가 인터프리터와 컴파일러가 어떻게 동작하는지에 대한 머릿속 골격에 살을 붙이기 위해 파고들었던 수많은 블로그 글, 사례 연구, GitHub Issues 스레드의 빙산의 일각에 불과하다. 프로그래밍 언어는 깊고 넓은 주제다! 기초만 아는 데서 멈추지 말고, 여러분의 흥미를 끄는 것을 진짜로 탐험하고 깊이 파고들어라. 처음엔 벅차 보이더라도.
앞서 말했듯, 초기 구현 이후에도 장난감 프로그래밍 언어는 여러분이 더 고급 기능, 개념, 최적화를 구현하며 마음껏 이것저것 만져볼 수 있는 엄청난 유연성과 창의적 여지를 제공한다. 원한다면 프로젝트의 확장으로 탐험해볼 만한 아이디어를 몇 가지 모아보았다.
더 정교한 타입 시스템. 타입 시스템을 공부하면서 나는 두 가지 관련 주제에 흥미가 생겼다. 범주론과, Haskell이나 Elm 같은 언어의 고급 타입 시스템으로 구현하는 우아한 데이터 구조들이다. Java, JavaScript, Python에서 왔다면 Swift, TypeScript, Rust에서 타입이 어떻게 동작하는지 공부하는 것이 좋은 출발점일 수 있다.
최적화. 인터프리터나 컴파일러를 어떻게 더 빠르게 만들 수 있을까? 그리고 빠르다는 건 대체 무엇을 의미할까? 파싱과 컴파일이 더 빠른 것인가? 최종적으로 더 빠른 코드를 만들어내는 것인가? 컴파일러 성능 주제는 선행 사례와 문헌이 끝도 없다.
C FFI(외부 함수 인터페이스) 추가하기. FFI는 여러분의 언어와 다른 언어(대개 C 바이너리) 사이의 상호운용 계층이다. C FFI를 추가하는 것은, 프로그램이 실행 파일로 컴파일되는 과정의 저수준 세부와 실행 파일 포맷, 코드 생성에 대해 배우는 좋은 계기가 될 수 있다.
JIT 컴파일. Chrome의 V8이나 Lua의 LuaJIT처럼 가장 흔하고 빠른 언어 런타임 중 일부는, 완전한 순수 인터프리터도 아니고 완전한 컴파일러도 아니다. 대신 실행 중에 즉석에서 머신 코드를 생성하는 하이브리드 방식의 JIT(Just-In-Time) 컴파일러다. JIT는 단순 인터프리터나 컴파일러보다 더 복잡하다는 대가를 치르지만, 프로그래밍 언어의 동적 특성과 성능 사이에서 더 나은 트레이드오프를 만들 수 있는 경우가 있다.
여기서 많은 내용을 다뤘지만, 이것이 내가 시작한 지점은 아니었고, 여러분도 자신만의 언어를 만들기 시작하기 전에 내가 공유한 모든 자료를 소화할 필요는 전혀 없다. 작게 시작하라. 야망을 갖고 창의적으로 하되, 똑똑하게 점진적으로 작업하라.
언어를 쓰기 시작하기 전에 모든 것을 이해할 필요는 없다. 내가 배운 것의 상당수는, 완전히 새롭고 낯선 주제에 뛰어들어 온라인에서 읽은 내용의 더 큰 부분을 이해할 때까지 계속 파고든 결과였다.
프로그래밍 언어는 다양성과 깊이 덕분에 내가 가장 좋아하는 컴퓨팅 분야 중 하나다. 그저 창의적으로, 좋은 질문을 하라 — 여러분이 자신만의 프로그래밍 도구를 만드는 가능성을 나만큼이나 마법처럼 느끼길 바란다.
← 복잡성 보존
나는 새 글을 뉴스레터로 공유한다. 이 글이 마음에 들었다면, 목록에 가입하는 것을 고려해보라.
댓글이나 답변이 있나요? 이메일을 보내주세요.