백엔드 프로젝트를 시작할 때 우리는 거의 반사적으로 데이터베이스부터 고릅니다. PostgreSQL을 붙일지, MySQL을 쓸지, 아니면 SQLite로 시작할지를 고민합니다. 그런데 DB Pro Blog의 이 글은 출발점을 아예 바꿉니다. “정말 데이터베이스가 필요한가?”가 아니라, 어차피 데이터베이스도 결국 파일인데 왜 처음부터 꼭 데이터베이스의 파일을 써야 하느냐 는 질문을 던집니다. 원문
글의 문제의식은 꽤 명확합니다. SQLite는 디스크 위의 단일 파일이고, PostgreSQL도 결국 여러 파일과 그 앞에 서 있는 프로세스일 뿐입니다. 즉 저장은 언제나 파일로 귀결됩니다. 그렇다면 많은 초기 제품, 내부 도구, 사이드 프로젝트, 또는 단일 서버에서 돌아가는 서비스는 데이터베이스 없이도 충분히 출발할 수 있지 않을까? DB Pro 팀은 이 질문을 단순 의견으로 끝내지 않고, Go·Bun·Rust로 같은 HTTP 서버를 만든 뒤 JSONL 파일과 메모리 맵, 디스크 기반 이진 탐색, SQLite를 실제로 벤치마크해 비교합니다.
Sources
1. 핵심 전제는 “데이터베이스도 결국 파일”이라는 사실이다
원문이 가장 먼저 강조하는 것은, 데이터베이스를 쓰든 안 쓰든 결국은 파일을 읽고 쓴다는 점입니다. SQLite는 말 그대로 하나의 파일이고, PostgreSQL도 파일들 위에서 동작하는 데이터베이스 엔진입니다. 그래서 질문은 “파일을 쓸 것인가”가 아니라, 데이터베이스가 관리하는 파일을 쓸 것인가, 아니면 내가 직접 관리하는 파일을 쓸 것인가 로 바뀝니다. 원문
이 관점은 꽤 중요합니다. 우리는 종종 파일 저장을 장난감처럼 보고 데이터베이스를 “진짜 저장소”처럼 생각하지만, 사실은 둘 다 같은 물리 계층 위에 있습니다. 차이는 저장 매체가 아니라, 그 위에 얹힌 인덱스, 동시성 제어, 쿼리 엔진, 트랜잭션, 다중 프로세스 조정 같은 기능입니다.
2. 가장 단순한 방식은 JSONL 파일을 매 요청마다 처음부터 끝까지 읽는 것이다
글에서 가장 먼저 실험한 것은 newline-delimited JSON, 즉 JSONL 파일입니다. users.jsonl, products.jsonl, orders.jsonl 같은 파일을 두고, 각 줄마다 레코드 하나를 저장합니다. 새 레코드는 append만 하면 됩니다. 조회는 요청이 들어올 때마다 파일을 열고, 한 줄씩 JSON으로 파싱하면서 원하는 ID를 찾을 때까지 스캔합니다. 원문
이 방식의 장점은 명백합니다. 구현이 거의 없습니다. 인덱스도 없고 설정도 없고 별도 프로세스도 필요 없습니다. 하지만 읽기 비용은 O(n)입니다. 레코드가 많아질수록 평균적으로 파일 절반쯤은 읽어야 원하는 데이터를 찾게 됩니다. 그래서 이 방식은 “가장 빨리 출발하는 방법”이지, 성장하는 읽기 부하를 감당하는 방법은 아닙니다.
3. 메모리 맵을 붙이면 파일이 곧 durability이고 맵이 곧 인덱스가 된다
두 번째 방식은 시작할 때 파일을 한 번 전부 읽어 해시 맵에 적재하는 방법입니다. 이후 조회는 메모리 맵에서 O(1) lookup으로 처리하고, 쓰기는 파일과 맵에 동시에 반영합니다. 프로세스가 재시작되면 파일에서 다시 로드하면 됩니다. 원문
이 구조를 원문은 간결하게 설명합니다. 파일은 durable backing store이고, 맵은 index라는 것입니다. 사실상 아주 단순한 데이터베이스를 직접 만드는 셈입니다. 저장은 append-only 파일에 하고, 읽기는 메모리 인덱스가 담당합니다. 단일 프로세스, 단순 키 조회, RAM에 충분히 들어오는 데이터셋이라는 조건에서는 이 방식이 놀랄 만큼 빠릅니다.
flowchart LR
W["Create / Update 요청"] --> F["JSONL 파일 append"]
W --> M["메모리 HashMap 갱신"]
R["Read 요청"] --> M
F --> B["재시작 시 다시 로드"]
B --> M4. RAM에 다 올리기 싫다면 디스크 위에서 이진 탐색하는 중간 지대가 있다
글에서 가장 흥미로운 부분은 세 번째 접근입니다. 메모리에 다 올리지 않으면서도, 파일 전체를 매번 스캔하지 않는 방법으로 정렬된 데이터 파일 + 고정 폭 인덱스 파일 + 디스크 위 이진 탐색 을 제안합니다. ID로 정렬된 JSONL 파일을 두고, 별도의 인덱스 파일에 <uuid>:<offset> 형태의 고정 길이 엔트리를 만들면 ReadAt으로 임의 위치를 바로 읽을 수 있습니다. 그렇게 하면 100만 건에서도 대략 20번 정도의 탐색만으로 원하는 레코드 위치를 찾을 수 있습니다. 원문
원문이 이 방식을 흥미롭게 보여 주는 이유는, 이것이 사실상 B-tree나 LSM-tree가 풀고 있는 문제의 단순화 버전이기 때문입니다. 정렬된 파일, 오프셋 인덱스, append가 정렬을 깨뜨린다는 점, 병합이 필요하다는 점까지 모두 데이터베이스 내부 구조와 닮아 있습니다. 즉 데이터베이스가 하는 일이 “마법”이 아니라는 것을 드러내는 좋은 예시입니다.
5. 벤치마크 결과는 직관적이면서도 약간 놀랍다
원문은 Apple M1 Mac mini, macOS 15 환경에서 10k, 100k, 1M 레코드에 대해 wrk -t4 -c50 -d10s로 GET 조회를 측정합니다. 결과는 대략 다음과 같습니다. 원문
- 선형 스캔은 데이터가 커질수록 급격히 느려집니다.
- Go는 10k에서 783 req/s, 1M에서 23 req/s
- Bun은 10k에서 469 req/s, 1M에서 19 req/s
- Rust는 10k에서 2,883 req/s, 1M에서 52 req/s
- 디스크 이진 탐색은 Go 기준 약 45k req/s에서 38k req/s 수준으로 거의 평평합니다.
- SQLite는 Go 기준 약 25k req/s 수준으로 매우 안정적입니다.
- 메모리 맵은 가장 빠릅니다.
- Go 약 97k req/s
- Bun 약 106k req/s
- Rust 약 169k req/s
평균 지연 시간도 같은 그림을 보여 줍니다. 선형 스캔은 1M 레코드에서 1초 안팎까지 치솟지만, 디스크 이진 탐색은 1.21.4ms, SQLite는 2.02.1ms, 메모리 맵은 서브밀리초를 유지합니다. 즉 “파일 기반 저장”이라고 해서 무조건 느린 것이 아니라, 어떤 인덱싱 전략을 쓰느냐 가 거의 전부라고 볼 수 있습니다.
6. 가장 놀라운 대목은 “잘 만든 파일 인덱스가 SQLite보다 빠를 수 있다”는 점이다
원문이 스스로도 예상 밖이었다고 말하는 부분은, 단순한 고정 폭 인덱스와 이진 탐색이 SQLite보다 약 1.7배 빠르게 나온 대목입니다. 물론 이 결과는 “순수한 primary key lookup”이라는 아주 좁은 작업에 한정됩니다. SQLite는 그 대신 SQL, B-tree, 다양한 쿼리, 트랜잭션, 동시성 제어 같은 훨씬 많은 기능을 제공합니다. 원문
그래서 이 결과를 “SQLite보다 파일이 낫다”로 읽으면 곤란합니다. 정확한 해석은 오히려 반대에 가깝습니다. 아주 단순한 문제 하나만 푸는 전용 구조는 범용 데이터베이스보다 빠를 수 있다. 하지만 데이터베이스는 그 성능 대신 엄청난 기능을 제공한다 는 것입니다.
7. 원문의 진짜 주장: 대부분의 제품은 생각보다 훨씬 작은 규모에 머문다
글 후반부에서 원문은 25,000 req/s가 실제로 어느 정도 규모인지 감각을 붙여 줍니다. 대략적인 가정 아래, SQLite 단일 서버가 감당하는 수준은 약 9천만 DAU에 해당한다고 계산합니다. 메모리 맵은 3억~6억 DAU 수준까지도 계산상으로는 버팁니다. 물론 이런 계산은 단순화된 가정이 많지만, 핵심 메시지는 분명합니다. 대부분의 제품은 여기에 한참 못 미친다 는 것입니다. 원문
예를 들어 원문은 1만 paying customer를 가진 SaaS가 하루 한 번씩 앱을 쓰는 정도라면 피크가 대략 3 req/s 수준이고, 10만 DAU 소비자 앱도 피크 30 req/s 정도라고 설명합니다. 이 가정이 모든 서비스에 맞는 것은 아니지만, 최소한 “처음부터 대형 분산 데이터베이스가 필요하다”는 직감이 얼마나 과장될 수 있는지는 잘 보여 줍니다.
8. 그럼 언제 진짜 데이터베이스가 필요한가
원문은 flat file이 영원히 충분하다고 말하지 않습니다. 오히려 한계를 꽤 선명하게 적습니다. 원문
첫째, 데이터셋이 RAM에 안 들어갈 때입니다. 메모리 맵 방식은 아주 빠르지만, 결국 인덱스를 메모리에 올릴 수 있어야 합니다.
둘째, ID 말고 다른 필드로도 자주 조회해야 할 때입니다. user_id 기준 주문 찾기, 가격 범위 조회, 날짜 필터링이 늘어나면 보조 인덱스를 계속 직접 만들어야 하고, 그 순간 사실상 쿼리 엔진을 직접 쓰기 시작하게 됩니다.
셋째, join이 필요할 때입니다. 여러 파일에서 데이터를 조합하는 일은 가능하지만 빠르게, 일관되게, 복잡한 조건까지 포함해 처리하려면 SQL이 훨씬 강합니다.
넷째, 여러 프로세스가 동시에 쓰기 시작할 때입니다. 단일 프로세스 내부의 RwLock 으로는 해결되지 않습니다. 여러 인스턴스가 동시에 실행되면 외부의 단일 truth source가 필요합니다.
다섯째, 여러 엔터티를 함께 묶는 atomic write가 필요할 때입니다. 주문 생성과 재고 차감을 반드시 같이 성공시키거나 같이 실패시켜야 한다면, 결국 트랜잭션이 필요해집니다.
실전 적용 포인트
초기 제품이나 내부 도구라면, 데이터베이스를 붙이기 전에 “실제로 필요한 연산이 ID 조회와 append뿐인가?”를 먼저 따져볼 만합니다. 그렇다면 JSONL + 메모리 인덱스 같은 구조로도 충분히 빠르게 출발할 수 있습니다.
파일 기반 저장을 선택하더라도 처음부터 선형 스캔만 고집할 필요는 없습니다. 정렬 파일과 간단한 인덱스만 추가해도 성능 특성은 완전히 달라집니다.
반대로 조회 조건이 빠르게 늘어나고, 여러 프로세스가 쓰고, 트랜잭션과 join이 필요해지기 시작하면 그때는 미련 없이 데이터베이스로 넘어가는 편이 낫습니다. 그 시점부터는 “DB를 안 쓰는 단순함”보다 “DB가 제공하는 기능”이 더 큰 가치가 됩니다.
또 하나 중요한 점은, 원문이 JSONL을 일부러 선택한 이유입니다. 나중에 데이터베이스로 옮기기 쉬워야 하기 때문입니다. 즉 파일 기반으로 시작하는 전략은 영구적인 반DB 선언이 아니라, 이행 가능한 임시 최적화 로 보는 편이 맞습니다.
핵심 요약
- 데이터베이스도 결국 파일 위에서 동작한다.
- 초기 제품에서는 데이터베이스 없이 JSONL 파일로도 충분히 시작할 수 있다.
- 선형 스캔은 가장 단순하지만 데이터가 커질수록 급격히 느려진다.
- 메모리 맵은 파일을 durability로, HashMap을 인덱스로 사용해 매우 빠른 조회를 제공한다.
- 디스크 기반 이진 탐색은 RAM을 아끼면서도 SQLite보다 빠를 수 있다.
- 하지만 SQLite는 속도만이 아니라 SQL, 인덱스, 조인, 트랜잭션, 다중 프로세스 조정을 제공한다.
- 데이터셋 크기, 다중 필드 조회, join, 다중 writer, atomic write가 필요해지면 데이터베이스가 필요하다.
결론
이 글의 매력은 “데이터베이스는 필요 없다”는 과격한 주장에 있지 않습니다. 오히려 왜 데이터베이스가 필요한지, 그리고 왜 때로는 아직 필요하지 않은지 를 아주 구체적으로 보여 준다는 점에 있습니다. 많은 제품은 생각보다 작은 규모에서 시작하고, 그 단계에서는 JSONL 파일과 메모리 인덱스만으로도 충분히 빠르고 단순한 구조를 만들 수 있습니다.
하지만 동시에 이 글은 데이터베이스의 가치를 더 분명히 보여 주기도 합니다. 우리가 데이터베이스에 돈과 복잡성을 지불하는 이유는 단순 저장 때문이 아니라, 인덱스, 쿼리, join, 동시성, 트랜잭션 같은 문제를 대신 풀어 주기 때문입니다. 결국 좋은 질문은 “DB를 쓸까 말까”가 아니라, 지금 내 제품이 정말로 어떤 문제까지 풀어야 하는가 일 것입니다.