제공해주신 문서는 SQLite의 database is locked 에러(특히 SQLITE_BUSY)를 해결하기 위한 기술 가이드로서 매우 훌륭한 구조를 갖추고 있습니다.

문서의 완성도를 높이기 위해 내용의 논리적 흐름을 정리하고, 누락된 '2. 해결 방법(Solution)' 섹션을 보완하여 전체 문서를 하나의 완성된 기술 백서 형태로 재구성해 드립니다.




[기술 가이드] SQLite database is locked 에러 분석 및 대응 전략



1. 개요 (Introduction)

SQLite 환경에서 발생하는 database is locked 에러는 주로 다중 프로세스 또는 다중 스레드가 동시에 데이터베이스 파일에 쓰기 작업을 시도할 때 발생하는 경합 상태(Contention)를 의미합니다. 본 문서는 이 에러의 근본 원인을 분석하고, 시스템의 안정성을 높이기 위한 단계별 대응 전략을 제시합니다로 합니다.

2. 근본 원인 분석 (Root Cause Analysis)

이 에러는 SQLite의 파일 기반 잠금(File-level Locking) 메커니즘에서 비롯됩니다.

  • 상태 전이의 충돌: SQLite는 SHARED (읽기), RESERVED (쓰기 준비), EXCLUSIVE (쓰기 중) 상태를 가집니다. 특정 프로세스가 EXCLUSIVE 상태를 점유한 상태에서 다른 프로세스가 쓰기를 시도하면 SQLITE_BUSY 에러가 발생합니다.
  • 잠금의 경합:
* Read-Write Conflict: 읽기 작업(SHARED)이 진행 중일 때 쓰기 작업(EXCLUSIVE)이 침범할 때. * Write-Write Conflict: 이미 쓰기 작업이 진행 중인 파일에 다른 프로세스가 쓰기를 시도할 때.
  • 트랜잭션 격리 수준의 한계: SQLite는 파일 단위 잠금을 사용하므로, 트랜잭션이 길어질수록 잠금 점유 시간이 늘어나 경합 확률이 기하급수적으로 증가합니다.


3. 단계별 해결 전략 (Solution Strategies)



단계 1: 트랜잭션 설계 최적화 (Immediate Action)

  • 트랜잭션 최소화: 트랜잭션 내에 네트워크 I/O, 복잡한 연산, 사용자 입력을 포함하지 마십시오. 오직 데이터베이스 작업만 포함하여 잠금 시간을 최소화해야 합니다.
  • Write-Ahead Logging (WAL) 모드 도입:
* 핵심: PRAGMA journal_mode=WAL; 설정을 통해 읽기와 쓰기가 서로를 방해하지 않도록 설정합니다. * 효과: 읽기 작업은 쓰기 작업을 방해하지 않으며, 쓰기 작업 또한 읽기 작업을 방해하지 않습니다. (동시성 극대화)

단계 2: 잠금 대기 시간 설정 (Configuration)

  • Busy Timeout 설정: 에러 발생 즉시 실패 처리하는 대신, 일정 시간 동안 재시도하도록 설정합니다.
* sqlite3_busy_timeout(db, 5000); (5초 동안 잠금이 풀리기를 기다림)
  • Immediate Transaction 사용: 쓰기 작업이 필요할 것이 확실하다면 BEGIN 대신 BEGIN IMMEDIATE를 사용하여 처음부터 쓰기 잠금을 획득하도록 유도합니다.


단계 3: 애플리케이션 계층의 방어 로직 (Application Layer)

  • 지수 백오프(Exponential Backoff) 구현: 에러 발생 시 재시도 간격을 점진적으로 늘려 경합을 완화합니다. (상세 구현은 4절 참조)


4. 운영 및 모니터링 가이드 (Implementation & Monitoring)



4.1. 재시도 로직 구현 예시 (Python)

에러 발생 시 무작정 재시도하는 것이 아니라, 시스템 부하를 고려한 전략적 재시도가 필요합니다.

import sqlite3
import time
import random

def execute_with_retry(db_path, query, params=(), max_retries=5):
    retries = 0
    while retries < max_retries:
        try:
            conn = sqlite3.connect(db_path, timeout=10) # 기본 timeout 설정
            cursor = conn.cursor()
            cursor.execute(query, params)
            conn.commit()
            return cursor
        except sqlite3.OperationalError as e:
            if "database is locked" in str(e):
                retries += 1
                # Exponential Backoff with Jitter
                wait_time = (2 ** retries) + (random.uniform(0, 1))
                print(f"Lock detected. Retrying in {wait_time:.2f}s...")
                time.sleep(wait_time)
            else:
                raise e
        finally:
            conn.close()


4.2. 체크리스트 (Troubleshooting Checklist)

  1. [ ] WAL 모드가 활성화되어 있는가? (PRAGMA journal_mode; 확인)
  2. [ ] 트랜잭션이 너무 길게 유지되고 있지 않은가? (Long-running transaction 확인)
  3. [ ] 다른 프로세스가 DB 파일을 독점하고 있지 않은가? (Backup 프로세스 등 확인)
  4. [ ] Busy Timeout 설정이 충분한가? (최소 3000ms 이상 권장)



결론: database is is locked 에러는 단순한 오류가 아니라 시스템의 동시성 한계를 나타내는 신호입니다. WAL 모드 적용적절한 Retry 로직만으로도 대부분의 문제를 해결할 수 있습니다.