SQLite WAL 모드 — 장점과 단점
Write-Ahead Logging이 SQLite의 동시성 문제를 어떻게 해결하는가
SQLite의 기본 모드(DELETE journal)에서는 write 중 read도 차단된다. 이 한계를 극복한 것이 WAL(Write-Ahead Logging) 모드다.
기본 원리
기존 DELETE 모드에서는 변경 사항을 DB 파일에 직접 쓰고, 원본을 저널 파일에 백업한다. WAL 모드에서는 반대로 변경 사항을 별도의 -wal 파일에 먼저 기록하고, 나중에 DB 파일에 반영(checkpoint)한다.
이 구조 덕분에 reader는 원본 DB 파일을 그대로 읽고, writer는 WAL 파일에 기록하므로 서로 블로킹하지 않는다.
Solid Queue와의 궁합22
Solid Queue는 백그라운드 잡을 SQLite에 저장하고, 빈번하게 INSERT/UPDATE를 수행한다. 이것이 WAL 모드가 해결하는 정확한 케이스다. DELETE 모드였다면 잡 처리 중 웹 요청의 DB 읽기가 블로킹되어 응답 지연이 발생했을 것이다.
WAL 모드 ON 하기
방법 1: Rails 8 (database.yml) — 권장
config/database.yml의 pragmas에 journal_mode: wal을 설정한다. Rails 8은 이것이 기본값이다.
방법 2: SQL로 직접 실행
sqlite3 storage/production.sqlite3 "PRAGMA journal_mode=WAL;"
Rails 콘솔에서도 가능: ActiveRecord::Base.connection.execute("PRAGMA journal_mode=WAL")
한 번 설정하면 DB 파일에 영구 저장된다. 앱을 재시작해도 WAL 모드가 유지된다.
WAL 모드 OFF 하기 (DELETE로 복귀)
sqlite3 storage/production.sqlite3 "PRAGMA journal_mode=DELETE;"
Rails: ActiveRecord::Base.connection.execute("PRAGMA journal_mode=DELETE")
주의: WAL → DELETE 전환 시 열려 있는 커넥션이 없어야 한다. 앱을 멈추고 실행하는 것이 안전하다. 전환되면 -wal, -shm 파일이 자동으로 삭제된다.
현재 모드 확인
sqlite3 storage/production.sqlite3 "PRAGMA journal_mode;"
Rails: ActiveRecord::Base.connection.execute("PRAGMA journal_mode").first
DELETE Journal vs WAL — 동작 방식 비교
| 항목 | DELETE (기본) | WAL 모드 |
|---|---|---|
| 쓰기 방식 | DB 파일에 직접 덮어쓰기 | -wal 파일에 append |
| Read + Write 동시 | 불가 (read도 차단) | 가능 |
| Write + Write 동시 | 불가 | 불가 (단일 Writer) |
| BusyException | 빈번 발생 | 대폭 감소 |
| 관리 파일 수 | DB + 저널 (2개) | DB + -wal + -shm (3개) |
| NFS 호환 | 호환 | 비호환 (corruption 위험) |
이 프로젝트에서의 판단
| 우려 사항 | 해당 여부 |
|---|---|
| 여러 머신에서 접근 | Fly.io 1대면 문제 없음 |
| WAL 파일 비대화 | SQLite가 자동 checkpoint함 (기본 1000 page) |
| 백업 주의 | sqlite3 .backup 명령 사용하면 안전 |
| Solid Queue write 경합 | WAL로 해결되는 정확한 케이스 |
결론: 단점보다 장점이 압도적인 상황. Rails 8의 SQLite 가이드에서도 프로덕션에서 WAL 모드를 권장하고 있다.
Rails 8 database.yml 기본 설정
production:
primary:
<<: *default
database: storage/production.sqlite3
pragmas:
journal_mode: wal
synchronous: normal
mmap_size: 134217728 # 128MB
journal_size_limit: 67108864 # 64MB
busy_timeout: 5000 # 5초
Checkpoint 종류
| 종류 | 동작 | 블로킹 |
|---|---|---|
| PASSIVE | 가능한 만큼만 반영, 방해 안 함 | 없음 |
| FULL | 모든 프레임 반영 (기본 자동) | 최소 |
| RESTART | FULL + WAL 처음부터 다시 시작 | 중간 |
| TRUNCATE | RESTART + WAL 파일 크기 0으로 | 높음 |
운영 체크리스트
busy_timeout 반드시 설정
설정 안 하면 Writer 잠금 충돌 시 즉시 SQLITE_BUSY 에러. Rails 8 기본값 5000ms.
journal_size_limit으로 WAL 비대화 방지
checkpoint 후에도 WAL 파일 크기가 유지되므로, journal_size_limit을 설정해 자동으로 잘라낸다.
백업은 sqlite3 .backup 사용
.sqlite3 파일만 복사하면 -wal에 기록된 최신 데이터가 누락된다. sqlite3 db.sqlite3 ".backup backup.sqlite3" 또는 Litestream 사용.
Fly.io 머신 1대 유지
WAL은 공유 메모리 락에 의존하므로 여러 머신에서 같은 DB 파일에 접근하면 corruption 위험. 단일 머신이면 전혀 문제 없음.
핵심 포인트
WAL 모드 활성화: PRAGMA journal_mode=WAL (Rails 8은 기본 설정)
reader와 writer가 동시에 접근 가능해짐 — DELETE 모드에서는 불가능했던 것
WAL 파일이 1000페이지(약 4MB)에 도달하면 자동 checkpoint로 DB에 반영
busy_timeout 설정으로 Writer 잠금 대기 시간 조절 (Rails 8: 5000ms 기본)
백업 시 .sqlite3 + -wal + -shm 파일 모두 포함 (sqlite3 .backup 명령 권장)
장점
- ✓ reader/writer 동시 접근 — DELETE 모드에서는 write 중 read도 차단됨
- ✓ write 성능 향상 — 작은 트랜잭션이 빈번한 경우 (Solid Queue가 정확히 이 케이스)
- ✓ BusyException 대폭 감소 — reader와 writer가 서로 블로킹하지 않으므로
- ✓ fsync 호출 감소 — checkpoint 시에만 필요하므로 I/O 부담이 줄어듦
- ✓ Rails 8 기본 설정 — 추가 구성 없이 바로 적용됨
단점
- ✗ WAL 파일 비대화 — checkpoint가 제때 안 되면 -wal 파일이 계속 커짐 (디스크 사용량 증가)
- ✗ NFS/네트워크 파일시스템 비호환 — 여러 머신에서 접근하면 corruption 위험 (Fly.io 1대면 문제 없음)
- ✗ 읽기가 아주 약간 느려질 수 있음 — WAL 파일도 확인해야 하므로 (실무에서 체감 거의 없음)
- ✗ 백업 시 주의 — .sqlite3 파일만 복사하면 안 되고 -wal, -shm 파일도 함께 복사 필요
- ✗ 파일 3개 관리 — DB파일 + -wal + -shm, 이동/복사 시 반드시 세트로 처리해야 함