개발/프로젝트

크래프톤 정글 × 바이브 프로젝트Mini SQL을 두 번 만들고 나서야보인 것들

cedis 2026. 4. 8. 23:07
Mini SQL을 두 번 만들고 나서야 보인 것들
크래프톤 정글 × 바이브 프로젝트
Mini SQL을 두 번 만들고 나서야
보인 것들
혼자 만든 최소 엔진과 팀 결과물을 나란히 놓고 보니,
이번 프로젝트의 핵심은 SQL 몇 문장을 처리하는 것보다 동작하는 구조를 어떻게 설명 가능한 형태로 바꿀 것인가에 더 가까웠다.
🧪 개인 최소 구현 👥 팀 결과물 🧩 직접 파싱 📈 Trace · Demo · Explainability

이번 글은 Mini SQL을 어떻게 구현했는지 순서대로 설명하는 글은 아니다. 이미 혼자 공부용으로 만든 버전이 하나 있었고, 이후 팀 결과물로 다시 같은 주제를 다뤘다. 두 저장소를 같이 놓고 보니 더 크게 남는 건 기능 목록보다 무엇을 일부러 단순하게 남겼고, 무엇은 굳이 구조를 나눠서 만들었는가였다. 혼자 만들 때는 끝까지 도는 최소 엔진이 먼저였고, 팀으로 만들 때는 그 엔진이 어떤 단계로 흐르는지 다른 사람도 볼 수 있어야 했다. 같은 "Mini SQL"인데도 기준이 달라지면 설계가 완전히 달라진다는 점이 이번 프로젝트를 가장 잘 설명해준다고 느꼈다. [개인 레포] [팀 레포]

이번 글의 관점
이번 회고의 중심은 "무엇을 더 많이 구현했는가"보다 왜 개인 버전은 직접적인 최소 구조를 택했고, 왜 팀 버전은 단계가 보이는 구조로 이동했는가에 있다.

이번 프로젝트를 그림으로 보면

글로 풀기 전에 그림으로 먼저 보면 두 버전의 차이가 더 빨리 들어온다. 개인 버전은 가능한 한 짧은 경로로 SQL 파일을 실행 결과까지 연결했고, 팀 버전은 그 사이 과정을 나눠서 각 단계가 눈에 보이게 만들었다.

개인 버전: 가장 짧은 왕복 경로
1
SQL 파일을 읽는다
입력은 최대한 단순하게 한 문장 SQL 파일로 제한했다.
2
cursor parser가 바로 읽는다
tokenizer를 따로 두지 않고 키워드와 값을 바로 소비한다.
3
작은 Statement 구조체를 만든다
필요한 정보만 담아 즉시 실행 단계로 넘긴다.
4
Executor가 CSV에 반영한다
헤더 기준 재배치와 full scan으로 최소 엔진 흐름을 완성한다.
입력에서 저장까지의 경로를 최대한 짧게 유지해서, 작은 SQL 처리기가 실제로 어떻게 끝까지 도는지 먼저 손에 잡는 구조였다.
팀 버전: 과정을 보여주는 구조
STEP 1
Tokenizer
문자열을 토큰으로 자른다.
STEP 2
Parser
토큰을 문법 구조로 바꾼다.
STEP 3
AST / Optimizer
중간 구조를 남기고 작게 다듬는다.
STEP 4
Executor / Storage
실제 파일 저장소에 반영한다.
STEP 5
Trace / Demo
중간 결과를 JSON과 화면으로 보여준다.
팀 결과물은 같은 SQL 처리기라도 각 단계를 나눠서 trace와 demo에 연결할 수 있도록 만들었다.

처음 개인 버전에서 제일 먼저 한 일은 범위를 아주 작게 자르는 것이었다

개인 공부용 레포를 다시 읽어보면, 가장 먼저 보이는 건 기능보다 범위를 잘라낸 흔적이다. 지원 문법은 INSERTSELECT뿐이고, 그것도 한 파일에 SQL 한 문장만 두는 형태다. CREATE TABLE은 구현하지 않고, 테이블 파일은 이미 있다고 가정한다. 겉으로 보면 빠진 게 많아 보이지만, 지금 생각하면 이 판단이 꽤 중요했다. 처음부터 SQL 전부를 흉내 내려고 하면 parser도 storage도 다 같이 커지는데, 그러면 "작은 SQL 처리기가 실제로 어떻게 끝까지 도는가"를 보기 전에 구조가 먼저 흐려진다. 개인 버전은 오히려 범위를 강하게 자른 덕분에 입력, 파싱, 실행, 저장이라는 핵심 흐름에 집중할 수 있었다. [Source]

구현 보고서에서도 이 버전을 "CSV 파일을 테이블처럼 쓰는 아주 단순한 row-store 파일 DB"라고 정리해 두었는데, 그 표현이 꽤 정확하다고 느꼈다. 이 프로젝트에서 처음 필요한 건 DB 이론을 전부 담는 게 아니라, SQL 텍스트를 읽어서 내부 구조로 바꾸고 파일에 반영하는 가장 짧은 왕복 경로를 하나 손에 넣는 일이었다. 결국 개인 버전의 설계는 빈약해서가 아니라, 처음 붙잡아야 할 질문을 흐리지 않기 위해 일부러 잘라낸 구조에 가까웠다. [Source]

파서는 tokenizer 없이 cursor 방식으로 바로 붙잡았다

개인 버전에서 가장 직접적인 선택은 parser였다. 팀 버전처럼 tokenizer를 먼저 두지 않고, parser.c에서 문자열 커서를 앞으로 밀어가며 키워드, 식별자, 값, 괄호를 바로 읽는다. consume_keyword(), parse_identifier(), parse_value_token()처럼 작은 함수는 따로 나뉘어 있지만, 전체 인상은 "토큰 배열을 한 번 더 만들지 않고 바로 구조체로 간다"에 가깝다. 이 방식은 확장성 면에선 불리할 수 있어도, 처음 직접 구현할 때는 SQL 문장을 눈으로 좇으면서 parser가 실제로 무슨 일을 하는지 보기에 훨씬 직관적이었다. [Source]

개인 parser가 문장을 읽는 방식
입력 문장
INSERT INTO users (id, name) VALUES ...
1단계
consume_keyword() 로 문장 골격을 확인한다.
2단계
parse_identifier(), parse_value_token() 으로 필요한 값만 추출한다.
3단계
토큰 스트림을 남기지 않고 바로 InsertStatement 를 채운다.
토큰 스트림을 따로 남기지 않고, 필요한 키워드와 식별자를 바로 소비하면서 statement 구조체를 채운다.

자료구조도 같은 방향이다. statement.h를 보면 StatementType과 tagged union 아래에 InsertStatement, SelectStatement를 둔다. 컬럼과 값도 고정 길이 배열에 저장한다. 즉 개인 버전의 파싱 결과는 거대한 AST가 아니라, 지금 이 과제 범위에서 필요한 정보만 담는 작은 statement 구조체다. 이 설계는 나중에 보면 단순해 보이지만, 그때는 오히려 이 단순함 덕분에 "SQL 문자열이 어떻게 내부 의미로 바뀌는가"를 처음 붙잡을 수 있었다. [Source]

개인 구현에서 중요했던 감각
처음에는 "범용적으로 예쁜 parser"보다, SQL 문장을 읽으며 지금 어느 토큰을 먹고 어떤 필드를 채우는지 손으로 따라갈 수 있는 구조가 더 중요했다.

저장 구조는 일부러 CSV 헤더 기준으로 단순하게 뒀다

저장 방식도 같은 맥락이었다. 개인 버전은 db/<table>.csv를 테이블 자체로 본다. 첫 줄은 헤더, 그 아래는 데이터 row다. load_header_columns()로 헤더를 읽고, find_column_index()로 컬럼 순서를 찾고, execute_insert()에서는 SQL에 적힌 컬럼 순서와 상관없이 CSV 헤더 순서대로 row를 다시 배치해 append 한다. 이렇게 두면 "테이블은 파일이고, 스키마는 헤더다"라는 규칙이 꽤 선명해진다. 별도 catalog도 없고 타입 시스템도 없지만, 대신 지금 어떤 정보가 스키마이고 어떤 정보가 row인지 설명하기는 쉬워진다. [Source] [Source]

INSERT가 저장될 때 실제로 벌어지는 일
입력
(name, id, age) 처럼 SQL에 적힌 컬럼 순서를 먼저 읽는다.
헤더 확인
id,name,age 를 읽어서 실제 저장 순서를 확인한다.
재배치
입력값을 헤더 순서로 다시 맞춘 row buffer를 만든다.
저장
1,Kim,20 같은 최종 row를 CSV 끝에 append 한다.
컬럼 이름을 먼저 해석하고, 실제 저장은 항상 헤더 순서대로 맞춘다. 단순하지만 "저장 규칙"이 눈에 보이는 방식이었다.

SELECT도 full scan 기반이다. 인덱스 없이 파일 전체를 읽고 필요한 컬럼만 출력한다. 지금 보면 비효율적이지만, 당시에는 이것도 꽤 의도적인 선택처럼 느껴진다. 처음부터 빠른 DB를 만드는 게 아니라, 문자열 명령이 실제 파일 저장소를 어떻게 건드리는지를 먼저 보는 게 목적이었기 때문이다. 그래서 개인 버전의 저장 설계는 성능보다 투명성에 더 가깝다. 느리더라도 현재 엔진이 무엇을 하고 있는지 설명 가능하면 그게 더 중요했다. [Source]

엔진과 프론트를 분리한 것도 지금 보면 꽤 중요한 결정이었다

개인 레포에서 인상적이었던 또 하나는 engine/frontend/를 아주 명확히 분리한 점이다. README는 프론트를 "발표용 시각화 데모 앱"이라고 부르고, 실제 구현 증명은 C 엔진과 테스트에 있다고 못 박는다. 당시에는 단지 발표를 편하게 하기 위한 분리처럼 보였는데, 지금 와서 보면 이 구분이 꽤 중요했다. 왜냐하면 SQL 처리기 자체와 그것을 설명하는 UI는 비슷해 보여도 책임이 전혀 다르기 때문이다. 엔진은 실제로 파일을 읽고 쓰고, 프론트는 그 과정을 사람이 이해할 수 있게 풀어주는 역할을 맡는다. [Source]

구현 보고서에서 "이 데모는 보조 시연용으로는 적합하지만, 최소 구현 자체의 증명으로 단독 사용하기는 부족하다"고 적어둔 대목도 기억에 남았다. 이 문장은 꽤 솔직했고, 동시에 정확했다. 발표 화면이 아무리 그럴듯해도 실제 엔진과 분리돼 있으면 설명과 구현의 연결이 약해진다. 개인 버전에서는 그 한계를 분명히 알고 있었고, 그 인식이 팀 버전에서 더 중요한 방향으로 이어졌다. [Source]

팀 결과물로 넘어오면서 핵심은 기능 추가보다 "과정이 보이는 구조"가 됐다

팀 레포의 README를 보면 방향이 확실히 달라진다. 여기서는 단순히 INSERTSELECT를 처리하는 게 아니라, 그 과정을 Tokenizer -> Parser -> AST -> Optimizer -> Executor -> Storage로 나눠서 설명한다. src/main.c도 실제로 이 순서를 orchestration하는 쪽에 가깝다. 입력은 SQL 파일이지만, 그 뒤의 책임은 각 모듈에 넘긴다. 개인 버전이 "한 프로그램 안에서 끝까지 도는 흐름"이었다면, 팀 결과물은 "각 단계가 무엇을 맡는지 다른 사람도 읽을 수 있는 흐름" 쪽으로 이동한 셈이다. [Source] [Source]

흥미로웠던 건, 무작정 구조를 키운 게 아니라는 점이다. README는 현재 버전을 의도적으로 AST-only 구조라고 설명한다. 즉 tokenizer와 parser, AST, optimizer, executor는 분리하지만 아직 Logical Plan 계층까지 만들지는 않는다. 이건 "더 복잡한 구조가 더 좋은 구조"라서가 아니라, 지금 설명해야 할 범위에 맞춰 필요한 만큼만 구조를 키운다는 태도로 읽혔다. 개인 버전에서 직접적인 흐름을 붙잡아본 경험이 있었기 때문에, 팀 버전의 이런 절제가 더 잘 납득됐다. [Source]

특히 달라진 건 "결과 화면"이 아니라 "실제 처리 과정"을 보여주는 방식이었다

두 레포를 나란히 놓고 가장 인상적이었던 차이는 여기였다. 개인 레포의 프론트는 스스로도 "설명용 보조 화면"이라고 정리돼 있다. 실제 엔진을 호출하지 않고, 최소 구현 증명은 CLI 실행기와 테스트 결과가 담당한다. 이건 당시 기준에선 충분히 자연스러운 선택이었다. 다만 팀 산출물 단계로 가면 이 방식만으로는 부족해진다. 화면이 그럴듯해도, 그 안에서 실제 엔진이 어떤 단계를 거쳤는지 연결이 약하면 설명이 끊기기 때문이다. [Source]

팀 버전 데모가 보여주는 것
SQL 입력
사용자가 준 원본 문장
token JSON
tokenizer가 쪼갠 결과
statement JSON
parser와 optimizer를 거친 구조
executor output
실행 결과와 콘솔 출력
table snapshot
실행 뒤 실제 데이터 상태
팀 버전에서는 결과만 출력하는 게 아니라, 그 결과가 어떤 중간 구조를 거쳐 나왔는지를 trace로 함께 보여준다.

팀 버전은 이 지점을 꽤 정면으로 해결한다. trace.c는 토큰 배열과 AST를 JSON으로 직렬화하고, demo_trace_main.c는 tokenizer, parser, optimizer, executor 각 단계의 결과를 하나의 trace JSON으로 남긴다. 여기에 tools/demo_server.py는 원본 data/를 바로 건드리지 않고 임시 workspace를 복제해 실행한다. 즉 데모 서버가 하는 일은 예쁜 화면을 띄우는 것보다, 실제 엔진 실행을 안전하게 관찰 가능하게 만드는 것에 더 가깝다. 이번 프로젝트를 지나며 나에게 남은 건 바로 이 차이였다. 그냥 돌아가는 것과, 돌아가는 과정을 다른 사람에게 보여줄 수 있는 것은 완전히 다른 기준이었다. [Source] [Source] [Source]

무게 중심을 그래프로 그리면

정량 측정은 아니지만, 이번 프로젝트를 지나며 두 버전의 무게 중심은 꽤 다르게 느껴졌다. 개인 버전은 직접 구현 감각과 최소 흐름 확보 쪽이 강했고, 팀 버전은 설명 가능성과 데모 연결성을 더 크게 끌어올린 구조였다.

개인 버전
팀 버전
직접 구현 감각
설명 가능성
데모와 실제 엔진의 연결
구조 분리

결국 어려웠던 건 SQL 자체보다 경계를 설명하는 일이었다

개인 버전에서는 어려움이 비교적 단순했다. SQL 문자열을 어디까지 지원할지, 헤더와 row를 어떻게 읽고 쓸지, CSV 순서를 어떻게 맞출지 같은 문제가 핵심이었다. 그런데 팀 버전에서는 "무엇을 구현할 것인가" 못지않게 "어느 단계가 무엇을 책임질 것인가"가 더 크게 다가온다. tokenizer는 어디까지 자르고, parser는 어디까지 구조화하고, optimizer는 무엇을 다듬고, executor는 어디까지 알아야 하는지가 모두 경계 문제로 바뀐다.

이 점은 optimizer.c가 오히려 잘 보여준다. 현재 optimizer가 하는 일은 projection 중복 제거처럼 작은 AST rewrite에 가깝다. 기능만 보면 소박하지만, 그 소박함이 의미 있었다. optimizer를 분리해 둔 이유가 "엄청난 최적화를 하겠다"가 아니라, parser와 executor 사이에도 분명한 역할 하나를 둘 수 있다는 사실을 보여주기 위해서처럼 보였기 때문이다. 팀 결과물이 더 어려웠던 이유는 기능이 더 많아서라기보다, 이런 경계들을 다른 사람도 납득할 수 있게 정리해야 했기 때문이라고 느꼈다. [Source]

개인 버전과 팀 버전을 같이 놓고 보면 무엇이 달라졌는가

구분 개인 공부용 레포 팀 결과물 레포 이번에 남은 이해
출발점 최소 SQL 처리기가 끝까지 돌게 만들기 그 과정을 다른 사람도 읽고 설명할 수 있게 만들기 "동작함"과 "설명 가능함"은 다른 기준이다
파싱 방식 cursor 기반 직접 파싱 + 작은 statement 구조체 tokenizer + parser + AST 처음엔 직접적이어야 보이고, 이후엔 단계가 보여야 설명된다
저장 구조 db/*.csv 헤더 기준 재배치 data/*.tbl + 단계별 trace 저장 포맷보다 중요한 건 저장소와 실행기의 관계를 어떻게 설명하느냐다
데모 성격 설명용 프론트, 실제 엔진과 분리 실제 trace와 snapshot을 보여주는 데모 서버 발표 화면도 결국 실제 엔진과 연결돼야 설득력이 생긴다
구조 철학 직접적이고 작은 구현 AST-only까지는 가되 과한 일반화는 미루는 절제된 구조 구조를 키울 때도 왜 여기까지인지 말할 수 있어야 한다

이번 프로젝트를 지나며 남은 건 "어떻게 보일 것인가"에 대한 기준이었다

이번 프로젝트를 지나며 가장 크게 달라진 건 구현을 보는 기준이었다. 예전 같았으면 SQL 몇 문장이 잘 처리되고 파일이 바뀌면 충분하다고 생각했을 것 같다. 그런데 개인 버전과 팀 결과물을 같이 겪고 나니, 이제는 그보다 먼저 묻게 된다. 이 프로그램은 처리 과정을 다른 사람에게 보여줄 수 있는가, 어느 단계에서 무슨 일이 벌어지는지 설명할 수 있는가, 데모가 실제 엔진과 연결돼 있는가, 구조를 키우더라도 왜 여기까지만 키웠는지 말할 수 있는가.

그래서 이번 Mini SQL은 단순히 작은 SQL 처리기를 만든 경험으로 남지 않았다. 혼자 만들 때는 직접적인 최소 엔진이 왜 필요한지 배웠고, 팀으로 다시 만들면서는 그 엔진을 설명 가능한 구조로 바꾸는 일이 또 다른 난도라는 걸 배웠다. 다음에 비슷한 프로젝트를 한다면 아마 더 빨리 "이게 돌아가느냐" 다음 질문으로 넘어갈 것 같다. 이 과정을 어떻게 보이게 만들 것인가, 이 구조를 어디까지 나눠야 설명과 시연이 동시에 성립하는가. 이번에는 그 질문이 꽤 오래 남았다.


참고 링크