데이터베이스가 정말 필요한가
(dbpro.app)- 모든 데이터베이스는 결국 파일 시스템 위의 구조화된 파일 집합으로, 초기 단계의 애플리케이션은 직접 파일을 관리해도 충분한 성능 확보 가능
- 동일한 서버를 Go, Bun, Rust로 구현해 파일 스캔·인메모리 맵·디스크 이진 탐색 세 가지 접근 방식을 비교한 결과, 단순 파일 접근만으로도 높은 처리량 달성 가능
- 인메모리 맵 방식이 최고 성능(최대 169k req/s) 을 보였으며, SQLite는 25k req/s로 안정적이지만 오버헤드 존재
- 대부분의 서비스는 SQLite 단일 파일로도 9천만 DAU 수준까지 처리 가능하며, 초기 제품 단계에서는 별도 데이터베이스가 불필요함
- 데이터셋이 RAM을 초과하거나 조인·다중 조건 검색·동시 쓰기·트랜잭션이 필요한 시점부터 데이터베이스 도입이 필요함
데이터베이스가 정말 필요한가
- 데이터베이스는 결국 파일 집합이며, SQLite는 단일 파일, PostgreSQL은 디렉터리와 프로세스로 구성됨
- 모든 데이터베이스는 파일시스템에 읽기·쓰기하며, 코드에서
open()을 호출하는 것과 동일한 방식으로 동작 - 따라서 핵심은 “파일을 쓸 것인가”가 아니라 “데이터베이스의 파일을 쓸 것인가, 직접 관리할 것인가”임
- 초기 단계의 많은 애플리케이션은 직접 관리해도 충분한 성능 확보 가능
- 모든 데이터베이스는 파일시스템에 읽기·쓰기하며, 코드에서
실험 구성
- 동일한 HTTP 서버를 Go, Bun(TypeScript), Rust로 구현하고 두 가지 저장 전략을 비교
users.jsonl,products.jsonl,orders.jsonl세 개의 JSONL 파일 사용POST /users로 생성,GET /users/:id로 조회- 조회 경로(GET)만 벤치마크 대상으로 설정
-
접근 방식 1: 매 요청마다 파일 읽기
- 요청 시 파일을 열고 모든 줄을 스캔하며 JSON 파싱 후 ID 일치 여부 확인
- 평균적으로 파일의 절반을 읽어야 하므로 O(n) 복잡도
- 데이터가 커질수록 요청 처리 속도 급격히 저하
-
접근 방식 2: 메모리에 전체 로드
- 시작 시 전체 파일을 읽어 ID 기반 해시맵에 저장
- 쓰기는 맵과 파일에 동시에 반영, 읽기는 단일 맵 조회로 O(1)
- 파일은 영속 저장소 역할, 맵은 인덱스 역할 수행
- Go의
sync.RWMutex, Rust의RwLock으로 병렬 읽기 지원
-
접근 방식 3: 디스크에서 이진 탐색
- 모든 데이터를 RAM에 올리지 않고도 빠른 조회를 위한 중간 해법
- ID 기준으로 정렬된 데이터 파일과 고정 폭 인덱스 파일(58바이트/레코드) 생성
ReadAt으로 인덱스를 O(log n) 탐색 후, 해당 오프셋에서 단일 레코드 읽기- 새로운 레코드 추가 시 정렬이 깨지므로 주기적 인덱스 재생성 또는 병합 필요
- 이 병합 패턴은 LSM-tree의 동작과 유사
벤치마크 환경
- 데이터셋 규모: 10k, 100k, 1M 레코드
- 부하 도구: wrk, 10초간 4스레드·50동시 연결로 무작위 GET 요청 수행
- 동일 머신(Apple M1 Mac mini, macOS 15)에서 Go 1.26, Bun 1.3, Rust 1.94로 테스트
- Go에서는 추가로 이진 탐색(디스크) 및 SQLite(modernc.org/sqlite) 비교
주요 결과
- 선형 스캔 성능 저하: 1M 레코드에서 Go 23 req/s, Bun 19 req/s로 급격히 느려짐
- 이진 탐색(디스크): 10k~1M 레코드 구간에서 45k→38k req/s로 15%만 감소
- OS 페이지 캐시 효과로 상위 인덱스 영역이 항상 메모리에 유지
- SQLite: 25k req/s, 평균 지연 2ms로 일관된 성능 유지
- 이진 탐색이 SQLite보다 약 1.7배 빠름, 단순 PK 조회에서는 SQLite의 오버헤드 존재
- 메모리 맵 방식이 최고 성능: 97k~169k req/s, 지연 0.5ms 이하
- Bun이 Go보다 빠름: Bun 106k req/s, Go 97k req/s
- Bun은 JavaScriptCore + Zig(uWebSockets) 기반으로 libuv를 우회
- Rust가 선형 스캔에서 압도적: Go 대비 3~6배 빠름, JSON 파싱 및 I/O 효율 때문으로 추정
-
사용 사례별 최적 선택
- 절대 최고 처리량: Rust 인메모리 맵 (169k req/s)
- RAM 비적재 조건에서 최고: Go 이진 탐색 (~40k req/s)
- SQL 필요 시: SQLite (25k req/s)
- 가장 간단한 구현: Go 선형 스캔 (~20줄 코드)
25,000 req/s의 의미
- 일반 웹 트래픽은 피크:평균 = 2:1 비율 가정
- 평균 12,500 req/s → 피크 25,000 req/s 수준
- 활성 사용자가 시간당 10회 조회, 피크 시 동시 접속률 10%로 가정
- 피크 요청 수식: DAU × 0.000278
- 각 접근 방식의 포화 DAU 계산 결과
- Go 선형 스캔: 2.8M
- Go 이진 탐색: 144M
- SQLite: 90M
- Go 인메모리 맵: 349M
- Bun 인메모리 맵: 381M
- Rust 인메모리 맵: 608M
- 대부분의 제품은 이 수치에 도달하지 않음
- 예: 10,000명의 SaaS 고객 → 3 req/s, 100,000 DAU 앱 → 30 req/s
- 결론적으로 대부분의 초기 제품은 데이터베이스가 필요하지 않음
- 필요 시에도 SQLite 단일 파일로 9천만 DAU까지 처리 가능
데이터베이스가 필요한 시점
-
RAM에 데이터셋이 담기지 않을 때
- 수천만 레코드 이상이면 인덱스만으로도 수GB 필요
- 데이터 페이징이 필요하며, 데이터베이스가 이를 자동 처리
-
ID 외 필드로 조회가 필요할 때
- 다중 조건 검색 시 파일 스캔 또는 추가 맵 필요
- 여러 맵을 유지하면 사실상 쿼리 엔진을 직접 구현하는 셈
-
조인이 필요한 경우
- 여러 파일을 읽어 조합해야 하며, SQL이 더 효율적
-
다중 프로세스 동시 쓰기 시
- 각 인스턴스의 인메모리 맵이 분리되어 일관성 상실
- 외부 단일 진실 소스 필요 → 데이터베이스 역할
-
엔터티 간 원자적 쓰기 필요 시
- 주문 생성과 재고 차감의 동시 성공/실패 보장 필요
- 별도 트랜잭션 로그 구현이 필요하며, DB는 이를 ACID로 해결
- 이러한 제약이 없는 내부 도구, 사이드 프로젝트, 초기 제품은
- 단일 서버 RAM 내에서 충분히 동작 가능
- JSONL 파일은 이후 데이터베이스로 손쉽게 마이그레이션 가능
부록 및 코드 제공
- Go, Bun, Rust 서버 코드 포함
- 데이터 시드, 벤치마크 실행 스크립트(
run_bench.sh) 별도 제공 - ZIP 파일에는
go-server/,bun-server/,rust-server/,seed.ts포함 - 스크립트는 세 규모의 데이터를 시드하고, wrk로 부하 테스트 후 종료
DB Pro 관련 안내
-
DB Pro는 Mac, Windows, Linux용데이터베이스 클라이언트
- 쿼리, 탐색, 관리 기능을 통합 제공
- 협업형 웹 플랫폼 및 내장 AI 지원
- 최신 버전에서는 Val Town의 SQLite 데이터베이스 연결 지원
- v1.3.0에서는 데이터베이스 생성, 다중 쿼리 편집기, PlanetScale Vitess 연결 기능 추가
저는 아주 좋은 글이라고 생각합니다. 특히 저런 '숫자' 가 들어있는 자료는 귀하죠. 우리가 만드는코드, 가져다 쓰는 기술스택이 어떤 오버헤드가 있는지 '대충의 감이라도 있는' 개발자 보기가 쉽지 않은 시대인데, 즐겁게 읽었습니다.
그러게요 새로운 인사이트가 있을까 싶어 원문 봤는데도 이게 뭔...
메모리가 비싸니 디스크 쓰는 얘기라던가, 프로덕션 운영 안정성을 위해서라던가, 원자성이라던가
그런 베이직한 얘기로 서두를 여는 게 아니라 냅다 속도 비교를 하고 앉아있으니까 실소가 나옵니다
'우린 DB 팔지만 DB가 항상 필요한 건 아니에요!' 아티클 남기며 이런 얘기도 스스럼없이 한다는 마케팅하고 싶은 건지 ㅡㅅㅡ... 긍정적으로 보려 해도 가끔 시니컬해진달까요
벤치마크라도 얻었다 쳐야겠습니다
SQLite를 프로덕션 서버용으로 선택하는 순간부터 언제 갈아탈지를 끊임없이 고민해야됩니다.
옛날에는 DB 자체의 비용(서버 구입비, IDC, 라이센스 비용 등)이 비싸서 고민해볼 가치가 있었지만,
요즘은 소위 말하는 딸깍으로 구축이 가능한데 과연 고민할 필요가 있을까요?
Hacker News 의견들
-
이 글이 정말 마음에 듦. 컴퓨터가 얼마나 빠른지 잘 보여줌
다만 마지막 부분의 결론에는 동의하지 않음. 저자는 “여러 프로세스가 동시에 쓸 필요가 있다”는 제약이 많은 앱에는 해당되지 않는다고 했지만, 실제로는 초기 단계의 제품에서도 cron이나 message queue 같은 별도 워커가 동시에 쓰기를 해야 하는 경우가 많음
메인 서버만 쓰게 만들 수도 있지만, 그건 아키텍처 복잡성을 높임
그래서 순수한 스케일 관점에서는 저자 말에 동의하지만, 좀 더 넓게 보면 데이터베이스를 쓰는 게 낫다고 생각함. 특히 SQLite는 합리적인 선택임
스케일이 필요하면 자주 접근하는 데이터를 메모리에 캐시하면 됨. 내가 쓰는 조합은 SQLite + 인메모리 캐시임- 나도 비슷한 상황을 자주 겪음. 서버 하나로 충분하더라도 서버 이중화가 필요해지는 순간, 네트워크 스토리지가 필요해지고 결국 네트워크 접근 가능한 DB로 기울게 됨
S3가 가끔은 통하지만, 여전히 완전한 대체로 쓰기엔 제약이 많음 - 요즘은 새 프로젝트를 시작할 때 기본적으로 SQLite를 씀. 성능이 매우 빠르고, 나중에 규모가 커지면 Postgres로 옮기기도 쉬움
별도의 DB 서버를 관리하거나 백업할 필요가 없어서 훨씬 간단하고 저렴함 - Rust 1M 벤치마크를 보고 나서, 컴퓨터가 얼마나 빠른지 다시금 깨달았음
- 나도 비슷한 상황을 자주 겪음. 서버 하나로 충분하더라도 서버 이중화가 필요해지는 순간, 네트워크 스토리지가 필요해지고 결국 네트워크 접근 가능한 DB로 기울게 됨
-
SQLite를 정말 좋아하지만, 모든 문제의 해답은 아님을 깨달음
클라이언트 측 사전 앱을 만들며 SQLite wasm 포트를 써봤는데, DB 파일이 예상보다 크고 압축도 잘 안 됐으며 로딩도 느렸음
결국 원본 TSV 파일에서 직접 인덱스를 만들어 zstd로 압축하고, wasm에서 매번 압축 해제하는 방식으로 바꿈. 이게 SQLite보다 훨씬 빨랐음
모듈 크기도 800KB에서 52KB로 줄었고, 여러 인스턴스를 동시에 띄워도 부담이 없었음
문자열 검색에는 stringzilla를 썼는데, 말도 안 되게 빠름
SQLite는 훌륭하지만, 모든 상황의 정답은 아님 -
SQLite 벤치마크가 최적화가 덜 되어 있음
단순히db.SetMaxOpenConns(runtime.NumCPU()) db.SetMaxIdleConns(runtime.NumCPU())이렇게만 추가해도 내 머신에서 성능이 27,700 r/s에서 89,687 r/s로 뛰었음
prepared statement나 timestamp를 int로 바꿔봤지만 큰 차이는 없었음 -
글은 괜찮았지만, “모든 DB는 파일시스템에 open()으로 접근한다”는 부분은 정확하지 않음
SQLite 같은 앱은 mmap을 써서 파일을 메모리 공간에 직접 매핑함. 이 방식은 syscall을 건너뛰고 훨씬 빠르게 접근 가능함
글 후반부에서 파일 전체를 메모리에 읽는 과정을 설명하는데, mmap을 썼다면 더 나았을 것 같음- 글이 DB의 IO를 단순화해서 설명한 건 맞음
다만 mmap이 항상 더 낫다고 보긴 어려움. 어떤 사람은 OS API에 의존하기보다 애플리케이션 로직에서 직접 처리하길 선호함
관련 논문은 CMU의 mmap 연구 참고 - mmap이 사용하는 백엔드 스토어도 결국 파일시스템의 파일임
“open()처럼 동작한다”는 표현이 다소 단순화된 것이지, 기술적으로는 맞는 말임
- 글이 DB의 IO를 단순화해서 설명한 건 맞음
-
오래전에 Perl로 작은 판매 웹앱을 만들었는데, ISP 서버에 아무것도 설치할 수 없어서 파일 기반 해시를 사용했음
클라이언트가 20년 넘게 그걸 그대로 쓰다가 세상을 떠났고, 가족이 인수하면서 Wordpress로 바꿨음
마지막에 확인했을 때 주문이 수십만 건이었는데도 성능이 괜찮았음
하드웨어 발전 덕분에 이 해킹 같은 구조가 예상보다 오래 버텼음. 지금이라면 SQLite로도 충분했을 것 같음- 어떤 제품을 판매하던 사이트였는지 궁금함
-
직접 스토리지를 구현해보면 DB가 어떻게 동작하는지 이해할 수 있음
인덱스나 자료구조를 효율적으로 다뤄야 하고, 결국 “장난이 아니라면 처음부터 DB를 썼어야 했다”는 결론에 도달하게 됨 -
Relational Databases Aren’t Dinosaurs, They’re Sharks
작은 앱에서 얻는 미미한 이득보다, 바퀴를 다시 발명하는 시간 낭비가 훨씬 큼- 상어 vs 공룡 비유가 정말 적절함
백악기 시절 상어는 이미 지금과 거의 같은 형태였고, 이후에도 큰 변화 없이 살아남았음
반면 공룡과 익룡, 모사사우루스는 사라졌지만 상어·악어·대형 뱀은 최적화된 설계 덕분에 지금까지 거의 그대로 존재함
관계형 DB도 그런 존재라고 생각함
- 상어 vs 공룡 비유가 정말 적절함
-
이런 글을 읽는 게 즐거움
그래도 나는 여전히 99%의 경우 SQL과 트랜잭션이 있는 DB를 씀
다만 최근 개인 프로젝트에서는 YAML 파일 기반의 단순 파일 시스템으로 데이터를 관리해봤는데, 내 규모에서는 성능 문제가 전혀 없음
사람이 읽을 수 있고 diff가 가능한 게 성능보다 더 중요했음
그래도 대부분의 경우엔 쿼리 언어와 보장된 일관성을 가진 DB를 선택할 것임 -
결국 항상 DB의 기능과 ACID 보장이 필요해짐
가끔 레거시 플랫파일 스토어를 써야 할 때마다, 일관성·트랜잭션·쿼리 언어를 억지로 붙이느라 고생함. 결국 바퀴를 다시 만드는 셈임 -
원자성이 필요한 순간에는 DB가 필수임
파일시스템 위에서 원자적 쓰기를 구현하는 건 매우 취약함
이런 이유로 많은 DB가 크래시 시 데이터 손상 문제를 겪음. 예전에 Windows의 RocksDB가 그랬음- 파일에 원자적 변경이 필요하다면 그냥 SQLite를 쓰겠음
직접 구현하는 건 미친 짓처럼 느껴짐. OS API로 안전하게 쓰는 법을 배우는 게 좋겠지만, 요즘은 그게 너무 니치한 기술임
게다가 후임자가 그걸 유지보수하지 못할 가능성이 큼. 결국 DB로 갈아탈 것임 - 글의 코드는 언젠가 정전이라도 나면 빈 파일이 될 것임
최소한 같은 파일시스템 내에서 임시 파일에 쓰고, fsync 후 rename으로 교체해야 함 - 단순한 경우엔 그렇게 취약하지 않음
전체 DB를 임시 파일에 쓰고 flush 후 move로 교체하면 Unix에서는 원자적임
다만 이건 스케일이 전혀 안 됨. 작은 업데이트에도 전체 파일을 다시 써야 하고, 락 관리도 필요함. ACID의 일부만 해결함 - 이렇게 보면 이미 ACID의 A를 다루고 있는 셈임
참고로 OLAP DB인 DuckDB는 out-of-core 워크로드에서도 훌륭하게 동작함 - 2025년 기준으로 Linux + ext4는 단일 및 다중 블록 원자적 쓰기를 지원함
공식 문서 링크
- 파일에 원자적 변경이 필요하다면 그냥 SQLite를 쓰겠음