SQLite3 데이터베이스 락(Locking)과 'database is locked' 에러 완벽 해결 가이드 II



1. 소개 및 개요



섹션 작성 오류: Ollama Local Engine Timeout (15 minutes)

2. 핵심 구현 및 설정 방법



2. 핵심 구현 및 설정 방법



SQLite3에서 발생하는 `database is locked` 에러를 근본적으로 해결하기 위해서는 단순히 에러를 Catch하는 것을 넘어, 데이터베이스 엔진의 동작 방식(Journal Mode)과 트랜keys션의 시작 시점, 그리고 애플리케이션 레벨의 연결 관리 전략을 재설계해야 합니다. 본 섹션에서는 실무 환경에서 즉시 적용 가능한 4가지 핵심 설정 방법을 단계별로 안내합니다.

2.1. WAL(Write-Ahead Logging) 모드 활성화: 동시성 문제의 핵심 해결책



SQLite의 기본 저널링 모드인 `DELETE` 모드는 쓰기 작업 시 데이터베이스 전체에 락을 걸어 읽기 작업조차 차단합니다. 이를 해결하기 위한 가장 강력한 도구는 WAL(Write-Ahead Logging) 모드입니다. WAL 모드를 사용하면 읽기 작업(Reader)이 쓰기 작업(Writer)을 차단하지 않으며, 반대로 쓰기 작업도 읽기 작업을 방해하지 않아 동시성을 극적으로 향상시킵니다.

설정 방법: 데이터베이스 연결 직후 아래의 `PRAGMA` 명령을 실행하여 모드를 전환하십시오.

```sql -- WAL 모드로 전환 PRAGMA journal_mode = WAL;

-- 설정 확인 PRAGMA journal_mode; ```

구현 시 주의사항: * 파일 생성: WAL 모드를 활성화하면 `.sqlite` 파일 외에 `-wal` 및 `-shm` 파일이 생성됩니다. 백업이나 파일 이동 시 이 파일들을 반드시 함께 관리해야 데이터 유실을 방방지할 수 있습니다. * 체크포인트(Checkpoint): WAL 파일이 너무 커지면 성능이 저하될 수 있습니다. 주기적으로 `PRAGMA wal_checkpoint(PASSIVE);`를 호출하여 변경 사항을 메인 DB 파일로 병합하는 로직을 고려해야 합니다.

---

2.2. `busy_timeout` 설정을 통한 대기 로직 구현



`database is locked` 에러는 특정 프로세스가 락을 점유하고 있을 때, 다른 프로세스가 즉시 접근을 시도하면 발생합니다. 이때 에러를 즉시 던지지 않고, 락이 해제될 때까지 일정 시간 동안 재시도(Retry) 하도록 설정하는 것이 `busy_timeout`의 핵심입니다.

Python(sqlite3) 예제 코드:

```python import sqlite3

def get_connection(db_path): # 연결 생성 conn = sqlite3.connect(db_path) # busy_timeout을 5000ms(5초)로 설정 # 락이 걸려있을 경우 5초 동안은 에러를 내지 않고 대기하며 재시도함 conn.execute("PRAGMA busy_timeout = 5000") # WAL 모드 활성화 (동시성 확보) conn.execute("PRAGMA journal_mode = WAL") return conn

사용 예시

try: connection = get_connection("my_database.db") cursor = connection.cursor() cursor.execute("INSERT INTO logs (message) VALUES ('Critical Update')") connection/commit() except sqlite3.OperationalError as e: print(f"에러 발생: {e}") # 5초가 지나도 락이 안 풀릴 경우에만 실행됨 finally: connection.close() ```

---

2.3. 트랜잭션 격리 수준 및 시작 방식 최적화 (`BEGIN IMMEDIATE`)



기본적인 `BEGIN` 명령은 `DEFERRED` 모드로 동작합니다. 이는 트랜잭션을 시작할 때는 공유 잠금(Shared Lock)만 걸어두었다가, 실제 쓰기 작업이 발생할 때 배타적 잠금(Exclusive Lock)으로 승격시키려 시도합니다. 이때 이미 다른 트랜잭션이 읽기 작업을 하고 있다면 `SQLITE_BUSY` 에러가 발생합니다.

이를 방지하기 위해, 쓰기 작업이 포함된 트랜잭션은 시작 단계부터 `BEGIN IMMEDIATE`를 사용하여 즉시 배타적 잠금을 획득하도록 설정해야 합니다.

비교 가이드: * `BEGIN DEFERRED` (기본값): 읽기 전용 트랜잭션에 적합. 쓰기 시점에 락 승격 실패 가능성 높음. * `BEGIN IMMEDIATE`: 쓰기 작업이 예정된 트랜잭션에 필수. 시작 시점에 다른 쓰기 트랜잭션의 진입을 차단하여 `SQLITE_BUSY` 발생을 원천 봉쇄.

```python

쓰기 작업이 포함된 트랜잭션의 올바른 예시

def perform_write_transaction(conn): try: # 시작부터 즉시 쓰기 권한을 요청하여 락 경합을 방지 conn.execute("BEGIN IMMEDIATE") conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1") conn.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2") conn.commit() except Exception as e: conn.rollback() print(f"트랜잭션 실패: {e}") ```

---

2.4. 핵심 설정 요약 체크리스트



성공적인 SQLite 운영을 위해 아래 설정이 모두 적용되었는지 확인하십시오.

| 설정 항목 | 권장 설정 값 | 목적 | | :--- | :--- | :--- | | Journal Mode | `WAL` | 읽기/쓰기 동시성 극대화 | | Busy Timeout | `5000` (ms) | 락 경합 발생 시 자동 재시도 대기 시간 확보 | | Transaction Type | `BEGIN IMMEDIATE` | 쓰기 작업 시 락 승격 실패(Deadlock) 방지 | | Synchronous | `NORMAL` | WAL 모드 사용 시 성능과 안전성의 최적 균형 |

이와 같은 설정을 결합하면, SQLite 환경에서도 엔터프라이즈급 애플리케이션에 준하는 안정적인 데이터 무결성과 높은 동시성을 확보할 수 있습니다.

3. 모니터링 및 트러블슈팅



3. 모니터링 및 트러블슈팅



데이터베이스 락 문제는 단순히 에러 메시지가 출력되는 시점에 이미 문제가 발생한 상태입니다. 진정한 운영의 고수는 `database is locked` 에러가 발생하기 전, 시스템의 지연(Latency)과 트랜잭션의 경합을 미리 감지하고 대응합니다. 본 섹션에서는 실무 환경에서 SQLite3의 상태를 감시하는 방법과, 에러 발생 시 단계별로 적용할 수 있는 트러블슈팅 체크리스트를 다룹니다.

3.1. 실무 모니터링 전략: "에러 전조 증상 포착하기"



SQLite는 서버형 DB(PostgreSQL, MySQL)와 달리 별도의 모니터링 데몬이 없습니다. 따라서 애플리케이션 레벨에서 쿼리 실행 시간과 락 경합을 로깅하는 로직을 직접 구현해야 합니다.

#### 1) 쿼리 실행 시간 및 락 지연 로깅 (Python 예시) 단순히 쿼리를 실행하는 것에 그치지 않고, `trace_callback` 또는 데코레이터를 사용하여 특정 시간(예: 500ms) 이상 소요되는 쿼리를 추출해야 합니다. 이는 특정 트랜잭션이 락을 오래 점유하고 있음을 알리는 강력한 지표가 됩니다.

```python import sqlite3 import time import logging

로깅 설정

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger("SQLiteMonitor")

class MonitoredConnection: def __init__(self, db_path, threshold=0.5): self.conn = sqlite3.connect(db_path) self.threshold = threshold # 임계치 (초 단위)

def execute(self, sql, params=()): start_time = timerypt.time() try: cursor = self.conn.execute(sql, params) return cursor finally: duration = time.time() - start_time if duration > self.threshold: logger.warning(f"[SLOW QUERY DETECTED] Duration: {duration:.4f}s | SQL: {sql}") else: logger.debug(f"Query executed in {duration:.4f}s")

def __getattr__(self, name): return getattr(self.conn, name)

사용 예시

db = MonitoredConnection('app_database.db') db.execute("UPDATE heavy_table SET status = 'processing' WHERE id = 1") ```

#### 2) PRAGMA를 활용한 상태 점검 운영 중인 DB의 설정이 의도한 대로(예: WAL 모드) 유지되고 있는지 주기적으로 체크해야 합니다. * Journal Mode 확인: `PRAGMA journal_mode;` 명령을 통해 현재 `wal` 모드인지 확인합니다. 만약 `delete`나 `truncate`로 돌아가 있다면, 다른 프로세스가 DB를 독점적으로 열고 있을 가능성이 큽니다로. * Lock Status 확인: SQLite 자체에는 복잡한 Lock Graph를 보여주는 기능은 없으나, `PRAGMA database_list;` 등을 통해 연결된 파일 상태를 점검할 수 있습니다.

---

3.2. 'database is locked' 발생 시 단계별 트러블슈팅 가이드



`SQLITE_BUSY` (Error Code 5) 에러가 발생했다면, 아래의 4단계 체크리스트를 순차적으로 수행하십시오.

#### Step 1: Busy Timeout 설정 재검토 (즉각 조치) 가장 먼저 확인해야 할 것은 `busy_timeout` 설정입니다. 기본값이 너무 낮으면 아주 짧은 순간의 경합에도 에러를 뱉습니다. * Action: `connection.execute("PRAGMA busy_timeout = 5000;")` (5초)와 같이 충분한 대기 시간을 부여했는지 확인하십시오. * Tip: 서비스의 특성에 따라 5,000ms ~ 10,000ms 사이의 값을 권장합니다.

#### Step 2: 트랜잭션 범위(Scope) 최소화 (코드 수정) `database is locked`의 가장 흔한 원인은 "트랜잭션을 너무 길게 유지하는 것"입니다. * Bad Case: `BEGIN` -> 외부 API 호출(Network I/O) -> `COMMIT` * Good Case: `BEGIN` -> 로컬 데이터 처리 -> `COMMIT` -> 외부 API 호출 * Checklist: 트랜잭션 내부에서 네트워크 통신, 대규모 파일 I/O, 혹은 복잡한 연산이 포함되어 있는지 확인하고, 이를 트랜잭션 외부로 분리하십시오.

Step 3: 쓰기 작업의 직렬화 및 분산 확인 (Concurrency Check) 여러 프로세스나 스레드가 동시에 `INSERT/UPDATE/DELETE`를 수행하고 있다면, SQLite의 파일 잠금 메커니즘 한계에 부딪힐 수 있습니다. * Action: 쓰기 작업이 집중되는 시점에 `Write-Ahead Logging (WAL)` 모드가 활성화되어 있는지 반드시 확인하십시오. WAL 모드는 읽기 작업과 쓰기 작업이 서로를 방해하지 않도록 도와줍니다. * Command: `PRAGMA journal_mode=WAL;`

Step 4: 외부 프로세스 및 파일 시스템 점검 (Environment Check) 소프트웨어 로직 외적인 요인을 점검합니다. * Zombie Processes: 종료되지 않은 이전 프로세스가 DB 파일을 잡고(Lock) 있는지 `lsof | grep your_db_file` 명령어로 확인하십시오. * Network Drive (NFS/SMB): SQLite 파일이 네트워크 드라이브에 있다면, 파일 잠금(File Locking) 메커니즘이 불안정하여 `database is locked` 에러가 빈번하게 발생할 수 있습니다. 반드시 로컬 스토리지(SSD/HDD)에서 운영하십시오. * Antivirus/Backup Software: 백업 소프트웨어나 백신 프로그램이 DB 파일을 스캔하는 순간 잠금이 발생할 수 있습니다. DB 디렉토리를 검사 제외 대상으로 설정하십시오.

요약 가이드



| 증상 | 주요 원인 | 해결책 | | :--- | :--- | :--- | | 간헐적 발생 | 일시적인 쓰기 경합 | `busy_timeout` 상향, WAL 모드 활성화 | | 지속적 발생 | 긴 트랜잭션 유지 | 트랜잭션 내 I/O 제거, 로직 분리 | | 특정 시점 집중 발생 | 대량의 배치 작업 | 작업 스케줄링 분산, 쓰기 전용 프로세스 격리 | | 알 수 없는 에러 | 파일 시스템/외부 요인 | 로컬 스토리지 사용, 백신 제외 설정 |

---

결론



본 가이드에 기술된 단계별 설정을 참고하여 시스템 최적화를 완료하시기 바랍니다. 추가로 생기는 예외 상황이나 질문은 댓글로 남겨주시면 성심껏 조치해 드리겠습니다.