SQLite 동시성과 이를 고려해야 하는 이유
(jellyfin.org)- SQLite의 파일 기반 구조는 간단하지만, 동시에 여러 쓰기 작업을 수행할 때 잠금(locking) 충돌이 발생할 수 있음
 - Jellyfin은 오랫동안 SQLite를 사용해왔으나, 일부 시스템에서 트랜잭션 중 데이터베이스 잠김 오류로 인해 애플리케이션이 중단되는 문제가 발생
 - EF Core의 인터셉터(interceptor) 기능을 활용해 세 가지 잠금 전략(No-Lock, Optimistic, Pessimistic) 을 구현하여 문제를 완화
 - Optimistic 방식은 재시도 기반으로 성능 손실을 최소화하고, Pessimistic 방식은 안정성을 높이지만 속도 저하를 감수
 - 이 접근법은 다른 EF Core 애플리케이션에도 쉽게 적용 가능한 구조로, SQLite 동시성 문제 해결에 실질적 대안 제공
 
SQLite의 기본 구조와 제약
- SQLite는 애플리케이션 내부에서 실행되는 파일 기반 관계형 데이터베이스 엔진
- 하나의 파일에 모든 데이터를 저장하며, 별도의 서버 애플리케이션이 필요 없음
 
 - 단일 파일을 애플리케이션이 완전히 관리하므로, 동시에 여러 프로세스가 접근하면 충돌 위험 존재
 - 따라서 SQLite를 사용하는 애플리케이션은 동시에 하나의 쓰기 작업만 수행해야 함
 
Write-Ahead-Log(WAL) 모드
- SQLite는 WAL(Write-Ahead-Log) 기능을 통해 동시성 제약을 완화
- WAL 파일은 데이터베이스 변경 작업을 기록하는 저널 파일 역할 수행
 - 여러 쓰기 작업을 병렬로 큐잉하고, 읽기 시 WAL의 변경 내용을 적용
 
 - 그러나 WAL도 완벽하지 않으며, 특정 상황에서는 여전히 잠금 충돌이 발생
 
SQLite 트랜잭션 문제
- 트랜잭션은 변경 작업의 원자성 보장과 읽기 차단 제어를 담당
 - Jellyfin 일부 시스템에서 트랜잭션 중 SQLite가 “database is locked” 오류를 반환하고 즉시 중단되는 현상 발생
- 이 문제는 운영체제, 디스크 속도, 가상화 여부와 무관하게 보고됨
 - 재현이 어렵고 불규칙하게 발생해 원인 규명이 어려움
 
 
Jellyfin의 SQLite 사용 방식과 문제점
- 권장 환경(비네트워크 스토리지, SSD)에서는 문제 발생이 드물지만, 10.11 이전 버전의 병렬 작업 제한 버그로 인해
- 라이브러리 스캔 작업이 과도하게 병렬 실행되어 수천 개의 동시 쓰기 요청이 발생
 - SQLite 엔진의 재시도 및 타임아웃 한계를 초과해 데이터베이스 과부하 및 오류 발생
 
 - 긴 트랜잭션과 비효율적 쿼리도 문제를 악화시킴
 
EF Core 기반 해결책
- Jellyfin은 EF Core로 코드베이스를 이전하면서 구조적 제어가 가능해짐
 - EF Core의 인터셉터(Interceptors) 를 활용해 모든 명령과 트랜잭션 실행을 가로채 투명한 잠금 제어 구현
 - 세 가지 잠금 전략을 도입
- No-Lock: 기본 모드, 별도 잠금 없음. 대부분의 경우 성능 저하 방지를 위해 사용
 - Optimistic Locking: 실패 시 Polly 라이브러리를 이용해 재시도 수행
 - Pessimistic Locking: 모든 쓰기 작업 전 ReaderWriterLockSlim으로 전체 데이터베이스 잠금
 
 
Optimistic Locking 동작 방식
- 작업 성공을 가정하고 실패 시 재시도하는 방식
- 두 개의 쓰기 작업이 충돌하면 한쪽이 실패 후 일정 시간 대기 후 재시도
 
 - Polly 라이브러리를 사용해 잠금으로 인한 실패만 재시도 대상으로 처리
 - Pessimistic 방식보다 오버헤드가 적고 성능 손실이 적음
 
Pessimistic Locking 동작 방식
- 모든 쓰기 작업 시점에 전체 데이터베이스를 잠금
- 쓰기 중에는 모든 읽기·쓰기 작업이 차단
 
 - 이 방식은 가장 안정적이지만 가장 느린 방식
- 예를 들어, “Alice” 테이블을 읽는 동안 “Bob” 테이블에 쓰기 가능하더라도 이를 허용하지 않음
 
 - ReaderWriterLockSlim을 사용해 다중 읽기 허용, 단일 쓰기만 허용
 
향후 계획: Smart Locking
- 
Optimistic과 Pessimistic 방식을 결합한 Smart Locking 도입 검토 중
- 두 방식의 장점을 통합해 성능과 안정성 균형 추구
 
 
결과 및 적용 가능성
- 초기 테스트 결과, 두 잠금 모드 모두 문제 해결에 효과적
 - 문제의 근본 원인은 여전히 불명확하지만, 이제 사용자가 안정적으로 Jellyfin을 사용할 수 있는 선택지 확보
 - 인터넷 상에서도 유사한 오류 보고가 많았으나, 완전한 해결책은 존재하지 않았음
 - Jellyfin의 구현은 EF Core 인터셉터 기반으로 손쉽게 복사·적용 가능한 구조
- 호출자는 내부 잠금 동작을 인식할 필요 없음
 
 - 동일한 SQLite 동시성 문제를 겪는 다른 EF Core 애플리케이션에서도 즉시 활용 가능
 
Hacker News 의견
- 
예전에 SQLite의 블로킹 문제를 겪었는데, 원인은 디스크 조각화(fragmentation) 였음
오래된 안드로이드 태블릿에서 앱을 몇 년간 매일 8시간씩 사용하던 사용자들이 느려짐과 락 오류를 호소했음
데이터를 복사해 받아보면 재현이 안 됐는데, 결국 기기를 직접 받아 확인해보니 DB 파일을 새 위치로 복사했다가 원래 이름으로 되돌리는 식으로 ‘디프래그’하니 문제가 완전히 사라졌음
Jellyfin DB에서도 같은 방법으로 성능 향상을 경험했음- 그건 조각화보다는 플래시 메모리 열화일 가능성이 높음. 혹시 eMMC 저장소가 들어간 저가형 태블릿이었는지 궁금함
 - SQLite의 VACUUM 함수로도 같은 효과를 낼 수 있는지 궁금함
 - 흥미로운 사례임. 하지만 사용자에게 직접 디프래그를 시킬 수는 없으니, 현실적인 해결책이 필요함
 
 - 
SQLite의 트랜잭션은 기본적으로 “deferred” 모드로 시작됨
즉, 실제 쓰기 작업을 시도하기 전까지는 write lock을 잡지 않음
SQLITE_BUSY오류는 읽기 트랜잭션이 쓰기로 전환하려 할 때 이미 다른 트랜잭션이 write lock을 잡고 있을 때 발생함
해결책은busy_timeout을 설정하고, 쓰기를 포함하는 트랜잭션은 “immediate” 모드로 시작하는 것임
관련 설명은 이 블로그 글에 잘 정리되어 있음- 나도 처음에 이건 
SQLITE_BUSY문제라고 생각했음. 관련 사례들을 여기에 모아두고 있음 - 
SQLITE_BUSY는 일종의 아키텍처 냄새라고 생각함. WAL 모드에서는 읽기 전용 커넥션 풀과 단일 쓰기 커넥션 풀을 분리해 설계함. 이렇게 하면 락 보유 상태를 명확히 파악하고, 경합 상황을 사전에 설계할 수 있음 - 
busy_timeout은 이 경우 적용되지 않음. WAL 모드에서는 페이지가 단일 로그 파일에 추가되므로, 읽기 중에 쓰기로 전환하려 하면 직렬화 보장을 위해 SQLite가 즉시 실패시킴. “immediate” 모드가 이를 방지함 - 결국 SQLite를 쓰는 사람이라면 언젠가 이 문제에 한 번쯤은 데이고 원인을 추적하느라 시간을 쓸 것임
 - 블로그 글에서 
SQLITE_BUSY언급이 없던데, 아마 설정이 빠졌던 것 같음 
 - 나도 처음에 이건 
 - 
글의 일부 설명이 잘못된 듯함
SQLite는 자체적으로 락 관리를 하기 때문에, 애플리케이션이 직접 파일 접근을 제어할 필요는 없음
또한 WAL은 여러 병렬 쓰기를 허용하지 않음. 단지 읽기와 단일 쓰기를 동시에 수행할 수 있게 해줌- 나도 SQLite를 정말 좋아하지만, 이 글은 기본적인 동시성 개념부터 틀려 있어서 추천을 못 하겠음
 
 - 
SQLite는 훌륭한 데이터베이스지만, 기본 설정값(defaults) 이 너무 보수적이라 아쉬움
실제 서비스용으로 쓰려면 여러 PRAGMA 설정을 조정해야 함- 어떤 PRAGMA를 기본으로 켜두면 좋은지 알고 싶음
 - 이런 상황이면 차라리 포크(fork) 해서 새 기본값을 만드는 게 낫지 않을까 싶음
 
 - 
SQLite의 새로운 hctree 기능이 안정화되면, 그때부터는 SQLite만 쓸 생각임
이름의hc는 아마 High Concurrency의 약자일 것 같음
공식 문서 링크 - 
이런 글을 보면 문제의 근본 원인 분석보다는 임시방편적인 해결책에 그친다는 느낌을 받음
더 깊은 디버깅과 연구로 정확한 원인을 밝혀내는 게 진짜 가치 있는 공유일 것 같음- 아마 글쓴이는 일부만 조사하고 불완전한 해결책을 공유한 듯함. HN에서 더 나은 답변을 유도하려는 의도였을 수도 있음. “틀린 답을 올리면 더 빨리 정답을 얻는다”는 말처럼 말임
 
 - 
WAL 모드도 결국 단일 쓰기, 다중 읽기 구조임을 이해하지 못한 듯함
병렬 쓰기는 불가능하고, 단지 읽기 트랜잭션이 쓰기에 의해 블록되지 않게 해줄 뿐임
완전한 MVCC가 있으면 좋겠지만, 현재 구조도 원리를 이해하면 충분히 잘 동작함 - 
나도 Jellyfin에서 비슷한 문제를 겪었음
평소엔 잘 돌아가지만, 특정 상황에서 DB가 락된 채 멈추는 현상이 생김
로그에는 “database is locked”만 남고, 결국 Docker 컨테이너를 재시작해야 해결됨
주로 TV UI에서 여러 버튼을 빠르게 조작할 때 발생함 - 
약간 다른 이야기지만, SQLite in-memory DB를 대량의 insert/delete 작업에 쓰면 메모리 사용량이 점점 늘어남
예를 들어 5분마다 10만 행을 넣고 지우는 작업을 며칠간 반복하면, macOS에서 메모리가 1GB까지 올라감
이런 경우 조정할 만한 설정이 있을지 궁금함- 혹시 VACUUM을 주기적으로 돌리고 있는지, 
auto_vacuum이 켜져 있는지 확인해보길 권함
VACUUM 문서 - 버퍼가 사용 패턴에 맞게 동적으로 조정되는 정상 동작일 수도 있음
 - 모든 행을 지우는 경우라면, 그냥 테이블을 드롭하고 다시 생성하는 게 더 효율적임
 
 - 혹시 VACUUM을 주기적으로 돌리고 있는지, 
 - 
SQLite는 훌륭하지만, 이런 문제를 보면 차라리 Postgres를 쓰는 게 낫지 않을까 싶을 때도 있음
단일 파일 이동성이나 임베디드 용도가 아니라면 Postgres가 더 간단하게 동시성 문제를 해결함- 하지만 Jellyfin은 셀프호스팅 미디어 서버라서, Postgres를 요구하면 설치와 유지보수가 복잡해짐. SQLite가 더 적합함
 - Jellyfin은 대부분 가정용 단일 사용자 환경이므로 SQLite로 충분함. 다만 현재 설정이 최적은 아닌 듯함
 - SQLite의 장점을 무시하고 Postgres로 가자는 건, “캠핑하려는데 오두막을 지으라”는 말과 같음
 - Postgres를 쓰면 설치뿐 아니라 버전 업그레이드 시 마이그레이션도 신경 써야 함. SQLite는 이런 부담이 없음
 - Jellyfin은 최근 DB 코드를 Entity Framework로 리라이트해서, 앞으로는 DB 선택을 유연하게 할 수 있게 준비 중임