m4 매크로 처리기의 대안으로 만들어졌다.
매크로 처리기를 만들게 된 것은 기존에 만들었던 Gdmarp라고 하는 게임 기획 문서 자동화 스크립트를 완전한 프로그램으로서 재구성(refactor)하게 되면서 m4 처리기의 한계를 극복할 수 있는 대안을 찾지 못했기 때문이다. 기존의 스크립트는 매우 광범위하게 m4 처리기를 활용했는데 m4에는 여러가지 문제점이 있었고 이는 다음과 같다. - 소위 m4 quote라고 하는 괴상한 문법은 직관성을 크게 떨어뜨렸다. - m4는 외부 프로그램이었기 때문에 윈도우 플랫폼에서 추가적인 설치 과정이 필요했다. - m4에는 여러 기본적인 매크로 지원이 부족했기 때문에 여러 매크로를 직접 구현해야 했다. - m4의 기능성을 확장하기 위해서는 syscmd에 의존해야 했는데 이 방법은 근본적으로 unsafe했다.
m4를 대체할 DSP(Domain specific language) 찾기 위해서 고군분투 했지만 내 필요에 맞으며 C-api와 호환가능한 옵션을 찾기란 쉽지 않았다. 잠시간의 조사 이후에는 이 참에 직접 매크로 처리기를 만들어야 겠다고 결론내렸다.
Macro parser generator는 성공적이지 못했다.
처음에는 커뮤니티에서 많이 사용되는 라이브러리(crate) 를 사용하려고 했다. 모든 것을 처음부터 만드는 것은 매우 어리석은 일이라는 것은 짧지 않은 프로그래밍의 시간 동안 확실히 알게 된 교훈이었기 때문이다.
처음에는 lalrpop 이나 pest를 사용하려고 했다. 그러나 곧이어 매크로 처리기에 활용하기에는 어려움이 있다는 것을 깨닫게 되었다. 매크로 처리기가 처리해야 하는 파일들은 매크로 문법에 부합하는 텍스트와 전혀 상관없는 일반(Plain) 텍스트로 구성되었는데 상기한 크레이트들은 그 둘의 상태를 구분할 수 있도록 설계되지 않았기 때문이었다.
필자에게 남은 선택지는 단 하나였고 그건 새로운 문법과 처리기를 직접 만들어 내는 것이었다.
R4d는 순진하게 작성되었다.
그렇게 만들어진 매크로 처리기, r4d는 다소 순진하게 작성되었다. 필자의 실력으로는 매크로 처리기로 하여금 문법에 순응하는 텍스트와 그렇지 않는 텍스트가 혼재되어 있는 파일로부터 사용가능한 AST(Abstract Syntax Tree)를 만들어 내기에는 어려움이 있었다. 물론 제대로 컴파일러를 공부한다면 이야기는 달랐겠지만, r4d의 최우선 목표는 성능이 아니었기 때문에 결론적으로는 선형적으로 매크로 문법을 감지하고 확장하는 방법을 채택했다.
그 논리과정은 다음과 같았다.
- 모든 문장을 처리기의 커서가 반복하며 매크로 트리거를 찾는다.
- 매크로 트리거가 아닌 문자는 그대로 출력하며 매크로 트리거가 발견될 경우에는 출력하지 않고 차후의 모든 문자를 매크로의 일부로서 저장한다.
- 매크로가 완결될 경우, 즉 짝 있는 모든 괄호가 종료된 경우에는 매크로 인자를 토대로 매크로를 실행하고 그 결과를 원래 위치에 출력한다.
- 만약 제대로 된 매크로 문법에 부합하지 않는다면 저장되었던 문자를 그대로 출력한다.
하지만 결과는 꽤 훌륭했다. 초기 버전만으로도 m4의 모든 사용례를 대체할 수 있을 정도로 r4d의 처리 기능은 튼튼했다. 더군다나 기본 매크로의 내부 구현이 일종의 함수 포인터로 구현이 되었기 때문에 새로운 기능을 추가하는 것 또한 매우 쉬운 일이었다. 그러다 보니 조금 욕심이 생겼고 m4의 사용례를 폭넓게 처리할 수 있도록 확장했다.
라이브러리 관리는 관심이 조금 필요하다.
Rust로 프로그램을 작성해 본적은 몇 번 있었지만 crates.io에 사용가능한 크레이트를 배포한 것은 처음이었다. 그러다보니 외부에 API가 공개되지 않는 바이너리를 작성할 때와는 전혀 다른 환경을 경험할 수 있었다.어떤 모듈과 구조체 그리고 함수를 API로 공개할 것인지 결정하는 것은 흥미로운 경험이었다.
다만 꽤나 신경 쓰였던 부분은 문서화 작업이었다. 바이너리는 간단한 사용례를 리드미에 적는 것으로도 충분했지만 라이브러리 같은 경우엔느 docs.rs에서 사용될 cargo doc을 활용한 문서화를 따로 해야했다. 또한 crates.io에 포함될 readme 같은 경우에는 배포할 경우에만 변경되었기 때문에 때때로 리드리 만을 수정하기 위해서 배포를 진행하기도 했다. 생각컨대 해당 크레이트를 위한 독립 도메인을 유지하는 것이 버전 관리 측면에서는 더욱 효율적이라는 생각이 들었다.
프로그램 보안에 대해서
내부 도구로 사용하는 프로그램을 작성할 때에는 전혀 생각하지 않았던 것이 바로 프로그램 보안(security)이었다. R4d는 시스템 커맨드와 파일 입출력을 매크로를 통해서 자유록게 지원하고 있고 이는 m4 처리기의 기능을 그대로 구현한 것이었다. 당시에는 프로그램 보안에 대해서 인식이 닿지 않았던 시절이기 때문에 m4는 사용자의 아무런 권한 없이도 해당 작업을 실행할 수 있었다. 하지만 지금의 시대는 Deno의 시대로 프로그램 보안 또한 고려되어야 할 요소 중에 하나였다. 그러한 고려 속에서 r4d는 사용자의 권한 부여 없이는 파일 입출력이나, 환경 변수 참조 그리고 시스템 커맨드 실행을 할 수 없도록 설계되었다.
선택적(Optional) 컴파일 플래그 활용
라이브러리 사용자는 자기가 쓰지 않을 기능을 사용하는 것을 별로 달가워 하지 않는다. 컴파일 시간이 길어지고 프로젝트 크기고 늘어나기 때문이다. 필자도 Game design engine을 제작하면서 reqwest라는 http 클라이언트 라이브러리를 사용했는데 dependenc만 70여개가 되는 것을 보고는 마음이 아팠던 기억이 있다. 그럼에도 reqwest의 강력한 기능 때문에 감수해야 했다.
그 기억을 토대로 해서 필자는 최대한 기능들을 플래그 별로 분리하여 사용자의 필요에 따라 의존성을 최대한 줄일 수 있도록 설계하게 되었다. 그래서 가장 기본적인 크레이트의 의존성 숫자는 26개로 모든 옵션을 포함할 경우의 숫자인 94개에 비하면 혁신적으로 적은 숫자이다.
디버깅 기능은 생각보다는 까다롭지 않은 일이었다.
역시나 외부로 공개했기 때문에 필요한 기능은 디버깅이었다. 작성자인 필자는 다소 난해한 에러 메시지를 보고서도 무엇이 잘못 되었는가를 찾을 의지가 충분했지만 r4d의 사용자도 그러리라는 확신은 하기 힘들었다. 그것을 염두에 두고 처음부터 이해하기 편한 에러 메시지를 만들었고 나중에는 디버깅 옵션을 통해서 디버깅 모드를 발동할 수 있도록 설계했다.
처음 기획을 할 때에는 디버깅이 제일 시간이 오래 걸릴 것이라고 생각했기 때문에 최대한 우선순위를 낮추어 생각했다. 그리고 더 이상 미룰 수 없을 때에 이르러서 본격적으로 개발을 하면서는 생각보다는 쉽다는 것에 안도했다. 디버깅 커맨드는 매우 제한된 문법으로 구현했기 때문에 match문 과 split만으로도 구현할 수 있었고 오히려 고생을 한 것은, 디버깅하고 있는 매크로 위치가 어디에 위치해 있는가를 제대로 출력하기 위한 로직이 까다로웠기 때문이었다.
어고노믹스를 생각한다는 것
또 하나 흥미로웠던 경험은 사용자의 어고노믹스를 고려하여 인터페이스를 설계하는 것이었다. 단순히 쓸모 있는 함수를 만드는 것이 아니라, 일관성있고 예측가능한 함수를 만드는 것이었다. 가장 대표적인 것은 빌더 패턴이었다. 구조체를 생성하면서 복잡한 인자를 전달하는 것은 직관적이지 못했고, 잘못 타이핑 하기도 쉬웠다. 반면 빌더 패턴은 매우 직관적이었으며 유연하게 데이터를 입력할 수 있었다. 한편 함수 인자의 타입을 설정할 때 고정된 타입으로 하는 것이 아닌, 트레이트와 AsRef을 조합하여 사용자에게 많은 선택지를 부여하는 것 또한 사용자 어고노믹스를 증진하는 데 도움이 되었다.
기타
R4d 링크
R4d를 만들게 된 이유인 Gdengine