프로그래밍 언어가 신기하면서도 미스터리한 도구로 느껴지시나요? 그 내부를 들여다보고 어떻게 작동하는지 이해할 기회가 생긴다면 어떨까요? 직접 코딩하며 프로그래밍 언어를 처음부터 끝까지 만들어 보는 것에 관심이 있다면, 이 글과 이어지는 시리즈가 큰 도움이 될 것입니다.
이 시리즈에서는 아주 간단한 인터프리터 방식의 동적 타입 프로그래밍 언어를 한 단계씩 차근차근 만들어 볼 예정입니다. 용어의 정확한 의미가 잘 와닿지 않거나 과제 자체가 다소 부담스럽게 느껴지더라도 걱정하지 마세요. 친숙하고 사랑받는 루비(Ruby)로 인터프리터를 구현하면서, 초보 개발자부터 숙련된 개발자까지 모두 따라올 수 있도록 각 단계를 명확하게 설명해 드립니다. 이 언어의 이름은 남아프리카공화국의 귀여운 허니배저(honey badger) 'Stoffle'을 기리기 위해 'Stoffle'로 정했습니다.
왜 프로그래밍 언어를 만들어야 할까?
솔직히 말해 Stoffle이 Python이나 Ruby를 대체할 가능성은 거의 없습니다. 그렇다면 굳이 만들 필요가 있을까요? 재미있다는 점을 제외하더라도, 이 시리즈를 통해 강조하고 싶은 것은 언어를 만드는 작업이 훌륭한 프로그래밍 연습이라는 사실입니다. 이 경험에서 얻을 수 있는 이점은 다음과 같습니다.
프로그래밍 언어(나아가 개발자 도구 전반)의 신비로운 베일을 벗겨 줍니다. 우리는 이러한 도구의 소비자일 뿐만 아니라, 필요하거나 원할 때 스스로 만들어 낼 수 있는 창조자이기도 합니다.
대부분의 개발자가 일상 업무에서 마주하지 않는 특별한 프로그래밍 문제들을 경험하게 됩니다.
프로그래밍 언어 구현 영역 밖에서도 유용한 컴포넌트들을 배울 수 있습니다. 예를 들어 파서(parser)는 수십 줄의 복잡한 정규 표현식 대신 활용할 수 있습니다. 상사가 "이제 시스템에 데이터를 가져오는 새로운 방식으로 저 골치 아픈 레거시 텍스트 파일을 지원해야 한다"고 말할 때 특히 유용하겠죠.
프로그래밍 언어는 어떻게 작동할까?
이 프로젝트에서 기다리고 있는 내용들을 살펴보기 전에, 먼저 시야를 넓혀 컴퓨터에서 프로그램이 어떻게 실행되는지 이해해 봅시다. 짐작하시겠지만, CPU는 Ruby 같은 고급 언어를 직접 지원하지 않습니다. 대신 CPU는 아키텍처에 따라 매우 저수준의 명령어 집합을 지원합니다.
궁금하시다면 x86 아키텍처를 검색해 보세요. PC나 Mac에서 이 글을 읽고 계신다면 여러분의 컴퓨터는 아마도 x86 아키텍처로 구동되고 있을 것입니다.
따라서 프로그래밍 언어의 임무는 고급 코드를 CPU가 이해할 수 있는 기계어(machine code)로 변환하는 것입니다. 이 작업을 수행하는 전략은 매우 다양합니다. Stoffle은 인터프리터 언어가 될 것이며, 이는 인터프리터가 프로그램이 실행되는 동안 Stoffle 소스 코드를 기계어로 번역한다는 의미입니다.
C 같은 컴파일 언어는 완전히 다른 방식으로 동작합니다. 컴파일 단계에서 소스 코드를 번역해 대상 CPU가 바로 실행할 수 있는 바이너리(즉, 기계어로 변환된 결과물)를 생성합니다. 또 다른 전략은 소스 파일을 이미 존재하는 다른 (주로 고급) 언어로 변환하는 것인데, 이를 흔히 '트랜스파일(transpile)'이라고 부릅니다.
다만 실제 세계의 언어들은 이렇게 깔끔하게 나뉘지 않는다는 점을 기억하세요. 현실의 언어들은 이러한(그리고 그 밖의) 여러 구현 전략의 요소와 기법을 다양한 방식으로 결합하는 경우가 일반적입니다. 기본기를 다 익힌 후에는 여러분이 선호하는 언어가 어떤 방식을 따르는지 직접 조사해 보시길 권합니다.
Stoffle 한눈에 살펴보기
앞서 언급했듯이 Stoffle은 아주 기본적인 인터프리터 방식의 동적 타입 프로그래밍 언어가 될 것입니다. 몇 가지 기본 데이터 타입, 4가지 산술 연산자, 비교 및 동등 연산, 논리 연산자, if/else, while 루프, 함수, 그리고 콘솔 출력 기능만으로 구성됩니다.
덧붙이자면, Stoffle처럼 학습과 실험이 주된 목적인 언어는 흔히 장난감 언어(toy language)라고 불립니다.
앞서 말씀드린 대로 Stoffle의 인터프리터는 우리의 든든한 친구 Ruby로 구현합니다. 인터프리터를 실행하면(stoffle hello_world.sfe) 소스 파일이 실행되기까지 거치는 구성 요소와 단계는 다음과 같습니다.

인터프리터의 구성 요소와 .sfe 파일이 실행될 때 일어나는 과정.
렉서(Lexer)
스캐너(scanner)라고도 불리는 렉서의 임무는 단순한 문자열을 '토큰(token)'이라 불리는 의미 있는 묶음으로 변환하는 것입니다. 예를 들어 my_var라는 변수를 선언했다고 상상해 봅시다. 렉서는 이 문자들을 읽어 Token::VARIABLE 토큰을 생성합니다.
파서(Parser)
소스 코드를 생각해 보면 중첩된 구조라는 점은 부정할 수 없습니다. 조건문을 예로 들면, 참/거짓 분기는 조건 평가 결과에 따라 중첩되어 실행됩니다.
파서의 주된 역할은 평평한 토큰 시퀀스를 토큰 간의 관계를 표현할 수 있는 데이터 구조로 변환하는 것입니다. 파서의 또 다른 중요한 기능은 문법 오류(syntax error)를 보고함으로써 코드에 문제가 있을 때 이를 알려주는 것입니다.
인터프리터(Interpreter)
Stoffle의 인터프리터는 단순하며, 파서가 생성한 데이터 구조를 직접 다룹니다. 인터프리터는 이 구조를 조각조각 분석하면서 실행해 나갑니다.
Stoffle에서 기계어로의 변환이 일어나는 이유는, 인터프리터 자체가 Ruby 프로그램이기 때문입니다. 즉, 우리가 인터프리터를 실행하면 Ruby의 인터프리터가 그것을 해석하는 셈이죠!
마치며
오늘 글에서는 Stoffle에 생명을 불어넣기 위해 우리가 밟아 나갈 단계들을 대략적으로 살펴보았습니다. 여러분도 저만큼 설렌 마음으로 함께해 주시길 바라며, 처음에는 프로그래밍 언어 구현이 범인(凡人)의 영역을 넘어선 일이라고 생각했던 분들에게 자신감을 심어줄 수 있었으면 좋겠습니다.
이 시리즈의 다음 글에서는 본격적으로 Stoffle의 렉서를 구현하며 시작할 것입니다. 다음 글이 끝날 무렵에는 Stoffle 소스 코드를 읽어 들여 단조로운 문자열을 좀 더 구조화된(그리고 훨씬 흥미로운!) 토큰 시퀀스로 변환하는 Ruby 프로그램을 완성하게 될 것입니다.
다음 편에서 다시 만나요!