SQLite의 -wal 파일이 계속 커질 때: 체크포인트와 끝나지 않는 읽기 트랜잭션
SQLite WAL 모드에서 -wal 파일이 줄지 않는 이유를 공식 문서로 정리했습니다. 자동 체크포인트 1000페이지, 읽기 트랜잭션이 체크포인트를 멈추는 원리, PASSIVE·FULL·RESTART·TRUNCATE의 차이와 journal_size_limit을 다룹니다.
SQLite 데이터베이스를 WAL 모드로 바꾸면 app.db 옆에 app.db-wal과 app.db-shm 파일이 생깁니다. 보통 -wal 파일은 일정 크기에서 더 커지지 않지만, 어떤 조건에서는 한없이 계속 커질 수 있습니다. 이 글은 2026년 10월 9일에 확인한 SQLite 공식 문서(Write-Ahead Logging, PRAGMA, sqlite3_wal_checkpoint_v2(), 파일 형식)를 기준으로, 체크포인트가 무엇을 하고 언제 멈추는지 정리합니다.
WAL 모드에서 쓰기와 읽기
데이터베이스 연결의 기본 저널 모드는 DELETE(롤백 저널)이고, PRAGMA journal_mode=WAL;로 바꿉니다. 다른 저널 모드와 달리 WAL 모드는 지속되어, 연결을 닫았다 다시 열어도 유지됩니다.
롤백 저널은 원래 내용을 저널에 복사해 두고 변경을 데이터베이스 파일에 바로 씁니다. WAL은 반대입니다. 데이터베이스 파일은 그대로 두고 변경된 페이지를 -wal 파일 끝에 덧붙이며, 커밋을 나타내는 레코드가 WAL에 추가되면 커밋된 것으로 봅니다. 파일 형식 문서의 용어로는 WAL에 기록되는 단위가 프레임이고, 프레임 하나는 페이지 하나의 새 내용입니다.
읽기 트랜잭션은 시작할 때 WAL에서 마지막으로 유효한 커밋 레코드의 위치를 기억합니다. 문서는 이것을 end mark라고 부르고, 트랜잭션이 끝날 때까지 바꾸지 않습니다. 그래서 다른 연결이 그 뒤에 커밋해도 이 읽기 트랜잭션은 시작 시점의 데이터를 봅니다. 페이지가 필요하면 end mark 이전의 WAL에서 그 페이지의 마지막 사본을 찾고, 없으면 데이터베이스 파일에서 읽습니다.
이 구조 덕분에 읽기와 쓰기가 서로를 막지 않습니다. 다만 WAL 파일이 하나뿐이라 쓰기는 한 번에 한 연결만 할 수 있습니다. 문서는 WAL 모드에서도 드물게 SQLITE_BUSY가 나는 경우를 따로 적어 두었으니, 쓰기 경합이 사라진다고 기대하면 안 됩니다.
체크포인트와 WAL 리셋
WAL에 쌓인 내용을 데이터베이스 파일로 옮기는 작업이 체크포인트입니다. 체크포인트는 먼저 WAL을 디스크에 동기화하고, 유효한 내용을 데이터베이스 파일에 옮긴 뒤, 데이터베이스 파일을 다시 동기화합니다.
기본 설정에서는 커밋 때문에 WAL이 1000페이지 이상이 되면 SQLite가 자동으로 체크포인트를 실행합니다. 마지막 연결이 닫힐 때도 체크포인트를 한 번 하고 WAL과 -shm 파일을 지웁니다. 1000이라는 기본값은 컴파일 옵션 SQLITE_DEFAULT_WAL_AUTOCHECKPOINT로 바꿀 수 있고, 실행 중에는 PRAGMA wal_autocheckpoint=N으로 바꿉니다. N이 0 이하이면 자동 체크포인트가 꺼집니다. 자동 체크포인트는 모두 PASSIVE 모드이고, 기본적으로 WAL을 한계 너머로 밀어낸 커밋을 실행한 스레드에서 돌아갑니다. 그래서 대부분의 커밋은 빠르고 가끔 하나가 눈에 띄게 느립니다.
체크포인트가 끝났다고 -wal 파일이 작아지지는 않습니다. 모든 내용이 데이터베이스로 옮겨지고 WAL을 쓰는 읽기 트랜잭션이 없으면, 다음 쓰기 트랜잭션이 WAL을 처음부터 덮어씁니다. 이것을 WAL 리셋이라고 합니다. 리셋할 때 WAL 헤더의 salt 값이 바뀌어, 이미 옮겨졌지만 아직 덮어쓰지 않은 옛 프레임은 무효가 됩니다. 파일을 줄이지 않는 이유는 기존 파일을 덮어쓰는 편이 늘리는 것보다 보통 빠르기 때문입니다. 문서는 1000페이지를 약 4MB로 설명합니다. 3.12.0부터 새 데이터베이스의 기본 페이지 크기가 4096바이트인 것과 맞는 수치이고, 실제 페이지 크기는 데이터베이스마다 다를 수 있습니다.
체크포인트가 끝까지 가지 못하는 이유
체크포인트는 읽기 트랜잭션과 동시에 돌 수 있지만, 진행 중인 어느 읽기 트랜잭션의 end mark보다 뒤에 있는 페이지에 닿으면 멈춥니다. 그 페이지를 데이터베이스 파일에 쓰면 그 읽기 트랜잭션이 보고 있는 내용을 덮어쓸 수 있기 때문입니다. 체크포인트는 어디까지 했는지 기억해 두었다가 다음에 그 자리부터 이어 갑니다.
공식 문서가 WAL이 한없이 커지는 경우로 드는 것은 세 가지입니다.
- 자동 체크포인트를 끈 경우: 컴파일 옵션이나 실행 시 설정으로 끄거나 미뤘다면 WAL이 커지는 것을 막을 장치가 없습니다.
- 체크포인트 기아: 읽기 트랜잭션이 겹쳐서 이어지는 바람에 활성 읽기 트랜잭션이 하나도 없는 순간이 오지 않으면 어떤 체크포인트도 완료되지 못하고, WAL을 리셋할 수 없습니다.
- 아주 큰 쓰기 트랜잭션: 쓰기 트랜잭션 도중에는 WAL을 리셋할 수 없으므로 큰 변경은 큰 WAL을 만듭니다. 3.11.0부터는 한 트랜잭션의 WAL 크기가 트랜잭션이 바꾼 양에 비례하고, 그 전 버전에서는 트랜잭션이 페이지 캐시보다 커지면 같은 페이지가 WAL에 여러 번 쓰일 수 있었습니다.
WAL이 크면 디스크만 차지하는 것이 아닙니다. 읽는 쪽은 페이지마다 WAL을 먼저 확인해야 하고, 문서에 따르면 그 시간은 WAL 크기에 비례합니다. 공유 메모리의 wal-index가 검색을 빠르게 해 주지만 WAL이 커지면 읽기 성능은 여전히 떨어집니다.
네 가지 체크포인트 모드와 NOOP
sqlite3_wal_checkpoint()와 자동 체크포인트는 항상 PASSIVE입니다. 다른 모드는 sqlite3_wal_checkpoint_v2()나 PRAGMA wal_checkpoint(모드)로 실행합니다.
| 모드 | 기다리는 것 | 쓰기 연결 | 완료 시 |
|---|---|---|---|
| PASSIVE | 기다리지 않음. busy handler를 부르지 않음 | 막지 않음 | 옮길 수 있는 만큼만 옮김. 모두 옮겼을 때만 데이터베이스 파일 동기화 |
| FULL | 쓰기 연결이 없고 모든 읽기 연결이 최신 스냅샷을 볼 때까지 | 대기 중 새 쓰기를 막음 | 모든 프레임을 옮기고 데이터베이스 파일 동기화 |
| RESTART | FULL에 더해, 옮긴 뒤 모든 읽기 연결이 WAL을 쓰지 않을 때까지 | 대기 중 새 쓰기를 막음 | 다음 쓰기 연결이 WAL을 처음부터 쓰게 됨 |
| TRUNCATE | RESTART와 같음 | 대기 중 새 쓰기를 막음 | 성공해서 반환하기 직전에 WAL 파일을 0바이트로 자름 |
현재 문서는 이 네 모드 말고 NOOP 모드도 따로 정의합니다. NOOP은 프레임을 하나도 옮기지 않아 체크포인트 작업을 전혀 하지 않고, 아래에서 설명할 WAL 프레임 수와 옮겨진 프레임 수를 얻기 위해서만 씁니다. sqlite3_wal_checkpoint_v2()에 넘기거나 PRAGMA wal_checkpoint(NOOP);로 실행합니다. 확인한 문서에는 NOOP이 어느 버전에 추가되었는지 적혀 있지 않으니, 쓰기 전에 사용하는 SQLite가 이 모드를 받아들이는지 확인해야 합니다.
FULL, RESTART, TRUNCATE는 쓰기 잠금을 얻고 읽기 연결을 기다리는 동안 busy handler를 부릅니다. busy handler가 0을 돌려주면 그 지점부터 PASSIVE처럼 할 수 있는 만큼만 옮기고 SQLITE_BUSY를 돌려줍니다. 다른 프로세스가 이미 체크포인트를 하고 있으면 체크포인트 잠금을 얻지 못하고, busy handler를 부르지 않은 채 곧바로 SQLITE_BUSY를 돌려줍니다. 이 기다림은 새 쓰기 연결을 막을 뿐 새 읽기 연결은 막지 않습니다. 다만 Write-Ahead Logging 문서는 RESTART와 TRUNCATE를 쓰면 체크포인트가 도는 동안 읽기 연결이 막힐 수 있다는 점을 단점으로 적고 있습니다.
PRAGMA wal_checkpoint 결과 읽기
PRAGMA wal_checkpoint(TRUNCATE);는 정수 세 개로 된 행 하나를 돌려줍니다.
- 첫 번째 열: 보통 0입니다. FULL, RESTART, TRUNCATE가 다른 연결 때문에 끝까지 가지 못했으면 1입니다. C API로 치면
SQLITE_OK면 0,SQLITE_BUSY면 1입니다. - 두 번째 열: WAL에 있는 프레임 수입니다.
- 세 번째 열: 그중 데이터베이스 파일로 옮겨진 프레임 수입니다. 이번 호출 전에 이미 옮겨진 것도 포함합니다.
WAL 모드가 아니면 두 번째와 세 번째 열은 -1입니다. TRUNCATE가 성공하면 WAL이 0바이트가 되므로 두 열 모두 0입니다. PASSIVE는 끝까지 가지 못해도 첫 번째 열이 1이 되지 않으므로, 세 번째 열이 두 번째 열보다 작은지를 봐야 합니다.
커진 WAL을 다루는 순서
- 먼저 끝나지 않은 읽기 트랜잭션을 찾습니다. 체크포인트 기아가 원인이라면 PASSIVE 체크포인트를 더 자주 돌려도 완료되지 않습니다. 문서는 아무도 읽지 않는 틈을 만들고 그때 체크포인트를 시도하라고 권합니다.
PRAGMA wal_autocheckpoint;로 자동 체크포인트가 꺼져 있지 않은지 확인합니다. 0 이하라면 앱이 직접 체크포인트를 돌리는지 봐야 합니다.- 읽기 연결이 많아 틈이 잘 생기지 않으면 RESTART나 TRUNCATE로 수동 체크포인트를 실행합니다. 이 모드는 busy handler로 기다리므로, 그 연결에
PRAGMA busy_timeout(또는sqlite3_busy_timeout())으로 busy handler를 설정해 둡니다. 결과의 첫 번째 열로 성공 여부를 확인합니다. - 커밋이나 체크포인트 뒤에 디스크에 남는 WAL 크기를 줄이려면
PRAGMA journal_size_limit=N(바이트)을 씁니다. 커밋하거나 WAL이 리셋될 때마다 남은 WAL이 N보다 크면 N으로 자릅니다. 남겨 두는 크기를 제한할 뿐 WAL이 커질 수 있는 최대 크기를 막는 상한은 아니어서, 큰 쓰기 트랜잭션이 진행되는 동안에는 WAL이 N보다 커질 수 있습니다. 기본값은 -1(제한 없음)이고, 0이면 매번 최소 크기로 자릅니다. 이 설정은 지정한 데이터베이스 하나에만 적용됩니다.
PRAGMA wal_autocheckpoint; -- 자동 체크포인트 간격(페이지 수)
PRAGMA journal_size_limit; -- 커밋·리셋 뒤 남길 WAL 크기(바이트), -1이면 제한 없음
PRAGMA busy_timeout = 5000; -- 예시 값(밀리초)
PRAGMA wal_checkpoint(TRUNCATE); -- 결과: 막힘 여부, WAL 프레임 수, 옮긴 프레임 수
-wal 파일을 직접 지우면 안 됩니다. WAL 파일은 데이터베이스의 영속 상태 일부이고, 데이터베이스 파일과 떨어지면 커밋된 트랜잭션을 잃거나 데이터베이스가 손상될 수 있습니다. 복사하거나 옮길 때도 함께 다뤄야 합니다. 공식 문서가 말하는 유일하게 안전한 제거 방법은 sqlite3_open() 계열 함수로 데이터베이스를 열고 곧바로 sqlite3_close()로 닫는 것입니다. 마지막 연결이 정상적으로 닫히면 체크포인트 뒤 WAL을 지우기 때문입니다. 프로세스가 비정상 종료했거나 SQLITE_FCNTL_PERSIST_WAL을 썼다면 연결이 모두 닫힌 뒤에도 WAL이 남을 수 있습니다.
체크포인트와 관련된 WAL-reset 버그
공식 문서에는 2026년 3월에 고친 WAL-reset 버그도 실려 있습니다. 같은 파일에 서로 다른 스레드나 프로세스의 연결이 둘 이상 열려 있고, 두 연결이 같은 순간에 쓰기나 체크포인트를 하면 드물게 데이터베이스가 손상될 수 있는 데이터 경합입니다. 3.7.0부터 3.51.2까지 있을 가능성이 높고, 3.51.3 이후에서 고쳐졌으며 3.44.6과 3.50.7에도 수정이 백포트되었습니다. 개발진은 흔한 사용에서는 일어날 가능성이 낮고 급한 일은 아니라고 하면서도, 결과가 심각하므로 업그레이드를 권합니다.
WAL 읽기 트랜잭션이 시작 시점의 데이터를 계속 보는 방식은 PostgreSQL의 스냅샷과 닮았습니다. PostgreSQL에서 스냅샷을 언제 잡는지는 READ COMMITTED와 스냅샷에서 다뤘습니다.
참고한 문서
- SQLite — Write-Ahead Logging: 1, 2.1, 2.2, 2.3, 3, 3.1, 3.2, 3.3, 4, 6, 9, 11절
- SQLite — PRAGMA Statements: wal_checkpoint, wal_autocheckpoint, journal_size_limit, journal_mode, busy_timeout, page_size
- SQLite — sqlite3_wal_checkpoint_v2()
- SQLite — Database File Format: 4.1, 4.3, 4.4, 4.5절
링크와 내용은 2026년 10월 9일에 확인했습니다.