# SQLite 동시성과 이를 고려해야 하는 이유

> Clean Markdown view of GeekNews topic #24087. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=24087](https://news.hada.io/topic?id=24087)
- GeekNews Markdown: [https://news.hada.io/topic/24087.md](https://news.hada.io/topic/24087.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-11-02T13:34:05+09:00
- Updated: 2025-11-02T13:34:05+09:00
- Original source: [jellyfin.org](https://jellyfin.org/posts/SQLite-locking/)
- Points: 11
- Comments: 2

## Summary

SQLite의 **파일 기반 구조**는 단순성과 휴대성을 제공하지만, 동시에 여러 쓰기 작업이 발생하면 **잠금 충돌(database lock)** 로 이어질 수 있습니다. Jellyfin 팀은 이 문제를 해결하기 위해 **EF Core 인터셉터**를 활용, **Optimistic·Pessimistic·No-Lock** 세 가지 전략을 구현해 안정성과 성능 사이의 균형점을 찾았습니다. 특히 **Optimistic Locking**은 재시도 기반으로 성능 저하를 최소화하면서도 충돌을 완화해, SQLite를 사용하는 다른 **EF Core 애플리케이션**에도 손쉽게 적용할 수 있는 실용적 접근을 제시합니다. 단순한 DB 설정 변경이 아닌, 프레임워크 수준에서 동시성을 제어하는 이 시도는 “가벼운 DB를 안정적으로 쓰는 법”에 대한 좋은 참고 사례로 느껴집니다.

## Topic Body

- **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)** 를 활용해 모든 명령과 트랜잭션 실행을 가로채 **투명한 잠금 제어** 구현  
- 세 가지 **잠금 전략**을 도입  
  1. **No-Lock**: 기본 모드, 별도 잠금 없음. 대부분의 경우 성능 저하 방지를 위해 사용  
  2. **Optimistic Locking**: 실패 시 **Polly 라이브러리**를 이용해 재시도 수행  
  3. **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 애플리케이션에서도 즉시 활용 가능**

## Comments



### Comment 45785

- Author: neo
- Created: 2025-11-02T13:34:06+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=45781298) 
- 예전에 **SQLite의 블로킹 문제**를 겪었는데, 원인은 디스크 **조각화(fragmentation)** 였음  
  오래된 안드로이드 태블릿에서 앱을 몇 년간 매일 8시간씩 사용하던 사용자들이 느려짐과 락 오류를 호소했음  
  데이터를 복사해 받아보면 재현이 안 됐는데, 결국 기기를 직접 받아 확인해보니 DB 파일을 새 위치로 복사했다가 원래 이름으로 되돌리는 식으로 ‘디프래그’하니 문제가 완전히 사라졌음  
  Jellyfin DB에서도 같은 방법으로 성능 향상을 경험했음
  - 그건 조각화보다는 **플래시 메모리 열화**일 가능성이 높음. 혹시 eMMC 저장소가 들어간 저가형 태블릿이었는지 궁금함
  - SQLite의 **VACUUM 함수**로도 같은 효과를 낼 수 있는지 궁금함
  - 흥미로운 사례임. 하지만 사용자에게 직접 디프래그를 시킬 수는 없으니, 현실적인 해결책이 필요함

- SQLite의 트랜잭션은 기본적으로 **“deferred” 모드**로 시작됨  
  즉, 실제 쓰기 작업을 시도하기 전까지는 write lock을 잡지 않음  
  `SQLITE_BUSY` 오류는 읽기 트랜잭션이 쓰기로 전환하려 할 때 이미 다른 트랜잭션이 write lock을 잡고 있을 때 발생함  
  해결책은 `busy_timeout`을 설정하고, 쓰기를 포함하는 트랜잭션은 “immediate” 모드로 시작하는 것임  
  관련 설명은 [이 블로그 글](https://zeroclarkthirty.com/2024-10-19-sqlite-database-is-lo...)에 잘 정리되어 있음
  - 나도 처음에 이건 `SQLITE_BUSY` 문제라고 생각했음. 관련 사례들을 [여기](https://simonwillison.net/tags/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**의 약자일 것 같음  
  [공식 문서 링크](https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html)

- 이런 글을 보면 문제의 **근본 원인 분석**보다는 임시방편적인 해결책에 그친다는 느낌을 받음  
  더 깊은 디버깅과 연구로 정확한 원인을 밝혀내는 게 진짜 가치 있는 공유일 것 같음
  - 아마 글쓴이는 일부만 조사하고 **불완전한 해결책**을 공유한 듯함. 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 문서](https://sqlite.org/lang_vacuum.html)
  - 버퍼가 사용 패턴에 맞게 **동적으로 조정**되는 정상 동작일 수도 있음
  - 모든 행을 지우는 경우라면, 그냥 **테이블을 드롭하고 다시 생성**하는 게 더 효율적임

- SQLite는 훌륭하지만, 이런 문제를 보면 차라리 **Postgres**를 쓰는 게 낫지 않을까 싶을 때도 있음  
  단일 파일 이동성이나 임베디드 용도가 아니라면 Postgres가 더 간단하게 동시성 문제를 해결함
  - 하지만 Jellyfin은 **셀프호스팅 미디어 서버**라서, Postgres를 요구하면 설치와 유지보수가 복잡해짐. SQLite가 더 적합함
  - Jellyfin은 대부분 **가정용 단일 사용자 환경**이므로 SQLite로 충분함. 다만 현재 설정이 최적은 아닌 듯함
  - SQLite의 장점을 무시하고 Postgres로 가자는 건, “캠핑하려는데 오두막을 지으라”는 말과 같음
  - Postgres를 쓰면 설치뿐 아니라 **버전 업그레이드 시 마이그레이션**도 신경 써야 함. SQLite는 이런 부담이 없음
  - Jellyfin은 최근 DB 코드를 **Entity Framework**로 리라이트해서, 앞으로는 DB 선택을 유연하게 할 수 있게 준비 중임

### Comment 45802

- Author: ndrgrd
- Created: 2025-11-03T09:39:40+09:00
- Points: 2

엥? 하는 부분이 있어서 바로 댓글부터 확인했더니 역시나...
