
Sevenwiki — The Knowledge Infrastructure
문서 편집부터 권한, 검색과 멀티 리전 구조까지 직접 설계한 위키 엔진
“위키피디아를 직접 만들어 보면 어떨까?”라는 말에서 시작해, 문법과 백엔드까지 새로 만든 프로젝트.
위키를 직접 만들어 보면 어떨까?
sevenwiki 이야기가 처음 나온 것은 2018년 무렵입니다.
당시 저는 HYU GEC, 한양대학교 소프트웨어 영재원에 다니고 있었습니다. 졸업할 무렵 팀 프로젝트를 함께했던 선배와 이야기를 나누다가 문득 이런 말이 나왔습니다.
위키피디아를 직접 만들어 보면 어떨까?
이 말이 나왔던게 거의 영재원 졸업당시기도 해서 얘기만 잠깐 나왔던 채 각자 학교생활로 바빠졌고, 자연스럽게 개발도 미뤄졌습니다.
하지만 고등학교 때 우연히 다시 연락이 닿았고, 예전에 이야기했던 위키를 이번에는 실제로 만들어 보기로 하여 2023년부터 본격적으로 개발을 시작했습니다.
이게 첫 번째 프로젝트였다고?
당시에는 Vulkan과 OpenGL을 이용한 그래픽 프로그래밍을 주로 하고 있었습니다. 웹 애플리케이션을 제대로 만들어 본 경험은 거의 없었습니다.
첫 버전은 Next.js와 FastAPI로 시작했습니다. Monaco Editor를 붙이고, 문서를 만들고 읽고 편집하는 기능부터 구현했습니다. 한국어권 위키에서 익숙하게 사용하는 문법과 편집 기능도 빠르게 따라 만들어 봤습니다.
처음에는 ‘글 정도만 git처럼 수정할 수 있는 그런 공간이면 되지 않을까’라는 생각을 하며 개발을 시작하였지만, 차례차례 기능을 추가하면서 이 일이 보통 일이 아님을 알게 되었습니다.
SEO를 생각하면 모든 글들은 미리 SSR된 상태로 내려와야했고, 모든 수정 기록은 DB의 인덱싱 성능을 낮추지 않으면서 (Postgres의 경우 한 field가 매우 커지면 인덱싱 성능이 저하되기에) 동시 편집 상태를 관리할 수 있어야하고, 파일 검증도 필요하며 (유저들의 개인정보 stripping, memory bomb 등등) 더더욱 가서는 이 위키를 위한 언어의 필요성까지 느끼게 되었습니다. 또한 위키의 include
같은 문서를 여러 사람이 편집할 수 있어야 했고, 모든 수정 기록을 보존해야 했습니다. 문서마다 읽기와 편집 권한이 달라야 했고, 검색 결과나 최근 변경에서도 비공개 문서가 노출되지 않아야 했습니다. 파일과 문서의 수명 주기도 따로 관리해야 했습니다.
계정없이 위키의 글을 작성하는 유동(IP) 유저들의 반달 행위를 잡아내기 위해서는 강력한 audit시스템 또한 설계해야했습니다.
결국 만들다 갈아엎었다 만들다 갈아엎었다를 5번정도 하게 되면서 점차 틀을 갖추고 실수를 줄여나가는, 제 스타일만의 백엔드 구조를 만들어나가게 되었습니다.
위키전용 언어를 만들어보자!
아무래도 공학도였던지라, 제 취향이 드러났던 탓일까요? 초반엔 위키 시스템 자체를 설계하는 것보다 위키 언어를 만드는 것이 더 즐거웠습니다.
대충 위키 MVP를 NextJS로 빠르게 만들어둔 채 RegExp로 급조된 문법들을 보니 너무나도 예외상황과 지저분한 케이스가 많더군요.
나름대로 나무위키를 최대한 호환시키기 위해 복잡한 정규식들도 들어가고 1-pass, 2-pass를 거치며 중첩구조도 잘 처리하게 만들었음에도 말이죠.
하지만 일단 나무위키가 {{{ {{{0}}} }}} 같은 다중첩 구조를 지원하는 이상 정규식으로는 한계가 있었습니다. 미리 패턴매칭을 시키며 stack에 쌓아두고, 닫힐 때마다 카운팅을 하는 방식도 있지만, 결국 context-aware한 파서를 만드는데는 무리가 있죠.
그렇기에, rayon을 사용해서 문단별 병렬 파서를 만들어보기도 (위키는 문단 단위로 context가 나뉘기 때문) ANTLR4를 사용한 EBNF 식 파서를 만들어보기도, 기타 여러가지 시도를 전부 해본 결과 결국 제게 제일 잘 맞고, 위키 언어에 가장 적합하다고 판단한 combinator parser방식의 winnow로 넘어오게 되었습니다.
이제 SevenMark는 문서를 바로 HTML로 바꾸지 않습니다. 통용되는 AST를 만들고, 이로부터 파생된 렌더러와 포매터, LSP 까지 같이 움직이는 필요한 것은 다 갖춘 DSL이 되었습니다.
물론 rust로 작성되었기에, WASM호환성이 좋아 웹에서도 바로 붙여내기 좋았구요.
없으면 만들어야지.
이는 아무래도 제 주력 언어가 Rust다보니 발생한 문제일텐데요. Rust의 생태계가 생태계인지라, 파이썬마냥 모든 것이 준비되어있는 경우는 거의 없었고, 특히 마이너한, 저와 같은 이런 niche한 케이스들의 경우 필요한 도구가 아예 없는 경우가 많았습니다.
그렇기에 위키의 blame 기능과 merge기능을 위한 threeway-merge-rs, blame-rs, 중간에 제 ACL실험을 위한 spicedb-rs-client 등 직접 필요한 생태계를 만들어나가며 개발을 진행하게 되었습니다.
이러다보니 중간중간 웹 생태계에서 빠져나와 원래 하던 시스템 관련의 프로그래밍으로 휴식을 취하는 느낌도 들고 재밌었던 것 같습니다.
이 시도들이 모두 현재 V7의 최종 구조에 남아 있는 것은 아닙니다. 권한 시스템은 여러 방식을 비교한 뒤 V7의 요구사항에 맞는 ordered ACL 구조로 다시 설계했습니다. 메시지 처리 역시 Kafka와 RabbitMQ 등을 검토한 뒤, 서비스 간 이벤트에는 NATS JetStream을 사용하고 Region 내부 작업은 PostgreSQL 큐로 처리하는 구조가 되었습니다.
V7. 멋진 이름이에요.
기능이 계속 늘어나자 기존 백엔드 구조를 조금씩 고치는 방식으로는 더 이상 관리하기 어려워졌습니다. 이 글 작성 당시 기준으로는 백엔드의 api 통신용 스펙 문서만 2MB에 가깝고 엔드도 200개 이상에 가까우니, 상당한 중대형 프로젝트라고 볼 수 있겠죠.
어느정도 엔진이 모습을 갖춰가던 무렵 이 백엔드의 이름을 주기로 결정하였습니다.
전 어떤 프로젝트를 진행할 때 이 이름 정하는 것을 굉장히 중요하게 생각하는데요, 마침 저희 위키 이름이 Sevenwiki이기도 했고, 구글의 JS엔진인 V8에 영감을 받아 V7 이라는 이름을 주기로 결정하였습니다.
V7엔진은 20개 이상의 다중 crate로 구성된 대형 프로젝트이며, 크게 보자면
Auth, Region, Media, Sitelink의 4가지 부분으로 나누어볼 수 있습니다.
해당 설계는 모든 리전의 위키가 같은 계정과 미디어 그리고 sitelink기능(위키피디아의 다국어 기능)을 사용할 수 있게 하면서도, 각 리전과 가까운 곳에 엣지서버를 두어 딜레이를 최소화시키기 위해 위 4가지의 큰 부류로 나누어 구조를 개편하게 되었습니다.
다이어그램으로 보자면, 
앞으로의 글에서는 제가 이 V7 엔진을 개발해오며 겪었던 문제점들, 그리고 해결한 방법, 또한 그리고 느낀점 정도를 풀어보며 글을 이어나가보고자 합니다.