SQLite의 격리(Isolation)
SQLite의 격리(Isolation)
격리(isolation)는 "한 연산이 데이터베이스에 가한 변경이 언제 다른 동시 연산에 보이게 되는지"를 정하는 속성이에요. SQLite는 대부분의 경우 서로 다른 데이터베이스 연결이 완전히 격리되는데, 그 규칙을 이해하면 동시성 버그를 피할 수 있어요. 여기서는 SQLite의 격리 모델과 잠금·저널 방식을 중심으로 살펴볼게요.
서로 다른 연결 간 격리
같은 데이터베이스를 두 개의 서로 다른 연결(sqlite3_open()으로 연 별개의 sqlite3 객체)로 읽고 쓴다고 해볼게요. 두 연결이 공유 캐시(shared cache)를 쓰지 않는다면, 읽는 쪽은 쓰는 쪽이 커밋한 완전한 트랜잭션만 볼 수 있어요. 커밋하지 않은 부분 변경은 읽는 쪽에 보이지 않아요. 이는 두 연결이 같은 스레드에 있든, 같은 프로세스의 다른 스레드에 있든, 다른 프로세스에 있든 똑같이 적용돼요.
공유 캐시 모드에서도, PRAGMA read_uncommitted가 꺼져 있는 한 같은 규칙이 성립해요. read_uncommitted는 기본적으로 꺼져 있어요. 공유 캐시 + read_uncommitted를 켠 조합이 유일하게, 한 연결이 다른 연결의 커밋 전 변경을 볼 수 있는 경우예요.
직렬화(Serializable) 트랜잭션
read_uncommitted가 켜진 공유 캐시 연결을 제외하면, SQLite의 모든 트랜잭션은 "serializable" 격리를 보여요. SQLite는 직렬화를 실제로 쓰기를 직렬화함으로써 구현해요. SQLite 데이터베이스에는 한 번에 하나의 작성자만 있을 수 있어요. 여러 연결이 동시에 열려 있고 모두 데이터베이스 파일에 쓸 수 있지만, 차례를 지켜야 해요. SQLite가 잠금으로 쓰기를 자동 직렬화하며, 애플리케이션이 신경 쓸 일은 아니에요.
저널 모드: 롤백 모드 vs WAL 모드
SQLite는 격리·동시성 제어·원자성을 데이터베이스 파일과 같은 디렉토리에 생기는 임시 저널 파일로 구현해요. 두 가지 주요 저널 모드가 있어요.
- 롤백 모드(rollback mode) —
journal_modepragma의DELETE,PERSIST,TRUNCATE옵션에 해당해요. 변경은 직접 데이터베이스 파일에 쓰이면서, 동시에 트랜잭션을 원래 상태로 되돌릴 수 있는 별도의 롤백 저널 파일이 만들어져요.DELETE모드(각 트랜잭션 종료 시 롤백 저널을 디스크에서 삭제)가 현재 기본 동작이에요. - WAL 모드 — 버전 3.7.0(2010-07-21)부터 지원돼요. 변경이 원본 데이터베이스 파일에 직접 쓰이지 않고, 별도의 "write-ahead log" 또는 WAL 파일로 들어가요. 트랜잭션 커밋 후 변경은 "checkpoint"라는 작업으로 WAL 파일에서 원본 데이터베이스로 옮겨져요. 활성화는
PRAGMA journal_mode=WAL.
롤백 모드에서 SQLite는 쓰기 트랜잭션이 진행되는 동안 데이터베이스 파일을 잠가서 다른 연결의 읽기를 막는 방식으로 격리를 구현해요. 쓰기가 디스크에 완전히 쓰이고 동기화되어 커밋된 뒤에야 읽는 쪽이 다시 들어올 수 있어서, 읽는 쪽은 절대 부분적으로 쓰인 변경을 보지 못해요.
WAL 모드는 동시 읽기와 쓰기를 허용해요. 변경이 원본 파일을 덮어쓰지 않고 WAL 파일로 가기 때문에, 읽는 쪽은 작성자가 WAL에 덧붙이는 동안에도 원본의 예전 내용을 그대로 읽을 수 있어요. WAL 모드에서는 "스냅샷 격리(snapshot isolation)"를 보여요. 읽기 트랜잭션이 시작하면 그 시점의 데이터베이스 "스냅샷"을 계속 보는 거예요. 읽기 트랜잭션이 진행되는 동안 커밋된 쓰기도 보이지 않죠.
같은 연결 안의 격리
격리는 서로 다른 연결 사이에만 존재해요. 같은 연결 안에서는 격리가 없어요. 예를 들어 한 연결이 BEGIN IMMEDIATE로 쓰기 트랜잭션을 시작하고 여러 UPDATE·DELETE·INSERT를 실행하면, 그 변경은 같은 연결의 이후 SELECT에 (커밋 전에도) 보여요. 다른 연결의 SELECT에는 커밋 전까지 안 보이죠.
한 연결 안에서 SELECT 문장은 시작 시점까지 완료된 모든 변경(커밋 여부와 무관)을 항상 봐요. 반면 같은 연결에서 SELECT가 실행되는 도중 발생한 변경을 그 SELECT가 보게 될지는 "정의되지 않은" 동작이에요. SQLite 버전·스키마·ANALYZE 실행 여부·쿼리 세부 사항에 따라 달라질 수 있어요. 그러니 이 경우를 가정하는 애플리케이션은 작성하지 않는 게 안전해요.
정리
- SQLite 트랜잭션은 직렬화(serializable)돼요.
- 한 연결의 변경은 커밋 전까지 다른 모든 연결에 보이지 않아요.
- 같은 연결의 쿼리는 쿼리 시작 전에 완료된 변경을 커밋 여부와 무관하게 봐요.
- 같은 연결에서 쿼리 시작 후 완료 전에 발생한 변경을 그 쿼리가 볼지는 정의되지 않아요.
- 공유 캐시 +
read_uncommitted를 켠 두 연결은 격리 관점에서 같은 연결로 취급돼요.
더 알아보기
- WAL 모드 상세: https://www.sqlite.org/wal.html
- 파일 잠금과 동시성: https://www.sqlite.org/lockingv3.html
- PRAGMA journal_mode: https://www.sqlite.org/pragma.html#pragma_journal_mode
- PRAGMA read_uncommitted: https://www.sqlite.org/pragma.html#pragma_read_uncommitted