Menu
Playground에서 시도하기

SQLite 자주 발생하는 에러 정리: database is locked부터 malformed까지

실무에서 진짜 마주치는 SQLite 에러들 — database is locked, readonly database, disk image malformed, 제약조건 위반까지 원인과 해결법을 한 번에 정리합니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

SQLite 에러, 사실은 친절한 신호다

SQLite가 뱉어내는 에러 메시지는 짧고 가끔은 암호처럼 보이지만, 실제로는 몇 가지 정해진 원인으로 거의 다 정리됩니다. 운영 환경에서 마주치는 문제는 대부분 다섯 가지 — 잠금(locking), 권한, 파일 손상, 스키마 불일치, 제약 조건 위반 — 안에 들어옵니다. 이 문서에서는 각 유형별로 어떤 상황에서 발생하는지, 메시지가 진짜로 의미하는 게 뭔지, 그리고 어떻게 해결하는지를 차근차근 살펴봅니다.

에러 문자열에는 숫자 코드가 함께 따라붙고, 확장 코드(extended code)를 보면 원인이 더 구체적으로 드러납니다. 로그에서는 보통 두 형태가 같이 찍힙니다:

Error: database is locked          -- 코드 5 (SQLITE_BUSY)
Error: unable to open database     -- 코드 14 (SQLITE_CANTOPEN)
Error: attempt to write a readonly -- 코드 8 (SQLITE_READONLY)
Error: database disk image is      -- 코드 11 (SQLITE_CORRUPT)

에러 코드를 알아두면 검색할 때 유리합니다 — 영어 메시지 그대로 검색하는 것보다 SQLITE_BUSY로 찾는 편이 훨씬 정확한 결과를 보여줍니다.

database is locked (SQLITE_BUSY)

여러 곳에서 동시에 쓰기 작업을 하는 애플리케이션이라면 가장 자주 마주치게 되는 SQLite 에러입니다. SQLite는 쓰기 작업을 직렬화하기 때문에, 같은 시점에 쓰기 락을 잡을 수 있는 커넥션은 단 하나뿐입니다. 두 번째 writer가 busy timeout 안에 락을 확보하지 못하면 바로 이 database is locked 에러가 발생합니다.

해결 방법은 효과가 큰 순서대로 세 가지입니다:

WAL 모드만 적용해도 대부분의 워크로드에서 잠금 문제는 해결됩니다. busy timeout은 실제로 쓰기 경합이 발생했을 때를 대비한 안전장치라고 보면 됩니다. 설정 외에도 코드를 한 번 점검해 보세요. 네트워크 I/O를 하는 동안 트랜잭션을 열어둔 채로 두면, 그 시간 내내 잠금이 유지됩니다. 트랜잭션은 최대한 짧게 유지하고, 작업이 끝나면 곧바로 COMMIT(또는 ROLLBACK)하는 습관을 들이세요.

unable to open database file (SQLITE_CANTOPEN)

SQLite가 파일을 열려고 했는데 OS가 거부한 경우입니다. 95%는 파일 경로나 해당 디렉터리가 문제입니다:

-- 確認すべきこと:
-- 1. パスは存在するか?            ls -l /path/to/db.sqlite
-- 2. 親ディレクトリは存在するか?  SQLiteはファイルを作成するが、
--    その上のディレクトリは作成しない。
-- 3. プロセスを実行しているユーザーは、ディレクトリに対して
--    読み取り+書き込み権限を持っているか(ファイルだけでなく)?
-- 4. ボリュームはマウントされ、満杯でなく、読み取り専用でないか?

미묘한 케이스가 하나 있습니다. SQLite는 데이터베이스 파일 옆에 보조 파일(-journal, -wal, -shm)을 만들어야 합니다. 그래서 파일 자체는 쓰기 가능하더라도 디렉터리에 쓰기 권한이 없으면, 열기는 성공하지만 쓰기에서 실패합니다. 디렉터리 단위로도 반드시 쓰기 권한을 부여하세요.

attempt to write a readonly database (SQLITE_READONLY)

앞선 에러와 사촌 격인 SQLite readonly database 문제입니다. 파일은 정상적으로 열렸는데 쓰기에서 막히는 경우죠. 원인은 빈도순으로 다음과 같습니다.

  • OS 사용자가 해당 파일 또는 디렉터리에 대한 쓰기 권한이 없는 경우
  • 읽기 전용 플래그로 연결을 연 경우 (SQLITE_OPEN_READONLY, 혹은 URI에서 mode=ro)
  • 볼륨 자체가 읽기 전용으로 마운트된 경우 (Docker bind mount이나 일부 클라우드 파일시스템에서 흔합니다)
  • 데이터베이스가 SQLite에 필요한 잠금(locking)을 지원하지 않는 네트워크 파일시스템에 위치한 경우

권한을 바로잡거나 볼륨을 다시 마운트하세요. Docker 환경이라면 바인드 마운트가 :ro로 설정돼 있지 않은지, 그리고 해당 디렉터리의 소유자가 컨테이너 사용자로 지정돼 있는지 확인해야 합니다.

database disk image is malformed (SQLITE_CORRUPT) — SQLite 파일 손상 복구

파일의 바이트가 더 이상 SQLite 포맷과 맞지 않는다는 뜻입니다. 원인은 대부분 환경적인 문제예요. fsync가 제대로 동작하지 않는 파일시스템에서 쓰기 도중 프로세스가 강제 종료됐거나, 쓰기 작업이 진행 중인 상태로 데이터베이스 파일을 복사했거나, 하드웨어 결함이 있었거나, Dropbox·iCloud 같은 클라우드로 파일을 동기화한 경우가 흔합니다.

먼저 손상 여부부터 확인해 봅시다:

integrity_check 결과가 ok로 나오면 DB 자체는 멀쩡한 상태라, 에러는 다른 곳(보통 오래된 커넥션이 남아 있는 경우)에서 발생한 겁니다. 반대로 문제 목록이 출력된다면 복구 작업이 필요합니다.

가장 깔끔한 SQLite 파일 손상 복구 방법은 CLI의 .recover 명령어를 쓰는 것입니다. 살릴 수 있는 데이터를 모두 새 데이터베이스로 뽑아내 줍니다:

sqlite3 corrupt.db ".recover" | sqlite3 recovered.db
sqlite3 recovered.db "PRAGMA integrity_check;"

최근 백업이 있다면 그냥 백업에서 복원하는 게 낫습니다. 훨씬 빠르고, "거의 다 복구된 것 같은데..." 같은 애매한 상황도 피할 수 있죠. 운영 중인 데이터베이스를 제대로 복사하는 방법(힌트: cp로 복사하면 안 됩니다)은 백업 및 복원 페이지를 참고하세요.

no such tableno such column 에러

말 그대로의 의미입니다만, 원인은 보통 둘 중 하나입니다. 내가 생각한 DB가 아닌 다른 데이터베이스에 연결되어 있거나, 마이그레이션이 실행되지 않은 경우죠.

애플리케이션의 연결 문자열을 먼저 확인해 보세요. 상대 경로는 현재 작업 디렉터리를 기준으로 평가되는데, 이 경로는 터미널, IDE, 프로덕션 프로세스에서 각각 다르게 잡힙니다. 그리고 인메모리 데이터베이스(:memory:)는 실행할 때마다 새로 생성되기 때문에, 데이터가 계속 남아 있을 거라고 기대했다가 당황하는 경우가 많습니다.

식별자에 따옴표를 붙이는 방식도 중요합니다. 따옴표 없이 쓴 이름은 대소문자를 구분하지 않지만, "User""user"는 서로 다른 식별자로 취급됩니다. 테이블을 만들 때 이름에 따옴표를 붙였다면, 이후에도 계속 따옴표로 감싸서 사용해야 합니다.

제약 조건 위반(Constraint violation)

SQLite는 제약 조건을 깨뜨리는 쓰기 작업을 거부합니다. 어떤 제약 조건이 문제인지는 에러 메시지에 그대로 드러납니다:

각 실패는 내부적으로 서로 다른 에러 코드를 갖습니다 (SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_NOTNULL). 해결은 거의 대부분 애플리케이션 계층에서 이뤄집니다. 즉, 데이터를 쓰기 전에 입력값을 먼저 검증하거나, 중복을 의도적으로 처리하고 싶다면 INSERT ... ON CONFLICT 구문을 활용하면 됩니다.

FOREIGN KEY constraint failed는 따로 짚고 넘어갈 필요가 있습니다. SQLite에서 외래 키는 기본적으로 꺼져 있습니다. 활성화하지 않으면 잘못된 참조가 조용히 들어가 버리고, 나중에 외래 키 검사를 켜는 순간에야 터지죠. 그래서 모든 커넥션마다 아래 pragma를 꼭 설정해야 합니다:

cannot start a transaction within a transaction

이미 트랜잭션이 열려 있는 상태에서 BEGIN을 또 호출하면 발생하는 에러입니다. SQLite는 중첩 트랜잭션(nested transaction)을 지원하지 않지만, 대신 세이브포인트(savepoint)를 중첩해서 쓸 수 있어 사실상 같은 효과를 낼 수 있습니다:

ORM이나 프레임워크가 트랜잭션을 직접 관리하는 구조라면, 트랜잭션을 두 번 시작하도록 코드를 짜놓았을 가능성이 큽니다. autocommit이 켜져 있는지, 그리고 커넥션 풀이 이미 트랜잭션이 열려 있는 커넥션을 재사용하고 있지는 않은지 확인해 보세요.

disk I/O error (SQLITE_IOERR)

OS가 읽기 또는 쓰기 요청을 거부한 경우입니다. 디스크가 가득 찼거나, 네트워크 파일시스템에서 일시적인 문제가 생겼거나, SQLite가 사용 중인 파일이 외부에서 삭제되어 버렸을 수도 있습니다. 가장 먼저 df -h로 디스크 용량부터 확인하세요. 그다음으로는 데이터베이스 파일이 NFS나 클라우드 동기화 폴더처럼 불안정한 위치에 있는지 살펴봅니다. SQLite는 fsync가 정상 동작하는 로컬 POSIX 파일시스템을 전제로 만들어졌기 때문에, 이런 환경을 피할 수 없다면 SQLite 파일 손상 위험이 높아진다는 점을 감수해야 합니다.

syntax error near "..."

SQLite 파서는 어떤 토큰에서 막혔는지 친절하게 알려줍니다. 그런데 정작 고쳐야 할 부분은 에러가 가리키는 위치보다 보통 두세 줄 앞에 있습니다. 빠진 쉼표, 따옴표로 감싸지 않아 예약어와 충돌한 식별자, 또는 작은따옴표 이스케이프를 빼먹은 문자열('it's'가 아니라 'it''s'처럼 써야 합니다) 같은 것들이 흔한 원인입니다.

사용자 입력은 문자열 연결로 SQL을 만들지 말고 파라미터 바인딩(? 플레이스홀더)을 사용하세요. 한 번의 조치로 SQL 문법 오류와 SQL 인젝션이라는 두 가지 위험을 동시에 피할 수 있습니다.

SQLite 에러 진단 체크리스트

운영 환경에서 문제가 터졌을 때, 아래 순서대로만 확인해도 대부분의 상황은 1분 안에 원인을 잡을 수 있습니다.

다섯 개의 PRAGMA, 다섯 개의 답. 실패한 쿼리의 에러 코드와 함께 보면, 문제가 어느 범주에 속하는지 그리고 다음에 어떤 문서 페이지를 펼쳐야 할지 바로 감이 옵니다.

커리큘럼 마무리하며

여기까지가 전체 여정입니다. CREATE TABLE로 시작해서 조인, 인덱스, 트랜잭션, WAL 모드, 백업을 거쳐, 마지막으로 SQLite를 실전에 투입했을 때 마주치게 되는 다양한 장애 상황까지 살펴봤습니다. 결국 핵심 패턴은 반복됩니다. 트랜잭션은 짧게, 외래 키는 켜두고, WAL 모드 활용, 정기적인 백업, 그리고 PRAGMA integrity_check에 대한 건강한 경외심. 이 습관만 잘 지키면 SQLite는 몇 년이고 조용히 제 몫을 해줄 겁니다.

자주 묻는 질문

SQLite에서 'database is locked' 에러는 왜 발생하나요?

다른 커넥션이 쓰기 락을 잡고 있어서 내 쪽이 대기하다 타임아웃된 상황입니다. 보통 세 가지로 해결합니다. 먼저 PRAGMA journal_mode=WAL로 WAL 모드를 켜서 읽기와 쓰기가 서로를 막지 않게 하고, PRAGMA busy_timeout = 5000으로 busy timeout을 늘려주고, 트랜잭션을 열어둔 채 방치하지 말고 바로바로 커밋하는 겁니다.

'attempt to write a readonly database' 에러는 어떻게 고치나요?

이건 거의 100% 파일시스템 권한 문제이지 SQLite 문제가 아닙니다. 프로세스를 실행하는 OS 사용자에게 DB 파일은 물론이고 그 파일이 들어있는 디렉터리까지 쓰기 권한이 있어야 합니다 (SQLite가 같은 디렉터리에 -journal이나 -wal 같은 사이드카 파일을 만들기 때문이죠). 소유자, 권한 비트, 그리고 해당 볼륨이 read-only로 마운트된 건 아닌지 확인하세요.

'database disk image is malformed'는 무슨 뜻인가요?

SQLite가 읽은 바이트가 예상한 포맷과 맞지 않다는 뜻입니다 — 보통 프로세스가 강제 종료됐거나, 디스크에 문제가 있거나, DB가 열려있는 상태에서 파일을 복사했을 때 생기는 손상입니다. PRAGMA integrity_check로 상태를 확인한 뒤, CLI에서 .recover 명령으로 살릴 수 있는 데이터를 새 DB에 덤프하는 방식으로 복구합니다. 백업이 있다면 그걸 복원하는 게 훨씬 빠릅니다.

'no such table'이나 'no such column' 에러가 뜨는 이유는 뭔가요?

내가 생각한 DB 파일이 아닌 다른 파일에 연결돼 있거나, 마이그레이션이 안 돌아간 경우가 대부분입니다. PRAGMA database_list로 SQLite가 실제로 연 파일 경로를 확인하고, .schema 테이블명으로 진짜 컬럼 구성을 보세요. 식별자 오타나 대소문자 불일치도 흔합니다 — 따옴표 없는 이름은 대소문자를 안 가리지만, 따옴표로 감싼 이름은 가립니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기