- SQLite는 로컬 저장소와 임베디드 프로젝트에 널리 쓰이는 자체 완결형 RDBMS지만, 하위 호환성 때문에 안전성과 성능에 불리한 기본값을 유지하고 있음
- 기본적으로 외래 키 제약을 강제하지 않고 ROWID를 재사용할 수 있어, 삭제된 사용자의 게시물이 같은 ID를 받은 다른 사용자에게 연결되는 데이터 무결성 문제가 생길 수 있음
- 일반 테이블은 선언된 열과 다른 자료형도 저장하며, STRICT 테이블은 이를 막지만 테이블마다 strict를 지정해야 하고 사용자 정의 타입 이름을 활용하던 방식과 충돌함
- 동시 쓰기에서 즉시 발생하는 SQLITE_BUSY, 비활성화된 WAL, 보수적인 동기화 설정은 각각 busy_timeout, journal_mode, synchronous 프라그마로 개선할 수 있음
- PRAGMA edition = 2026 같은 연도 기반 에디션으로 안전한 기본값 묶음을 선택하게 하면, 기존 동작을 깨지 않고 SQLite의 기본 설정을 계속 발전시킬 수 있음
SQLite의 강점과 낡은 기본값
- SQLite는 임베디드 프로젝트의 데이터베이스이자 로컬 데이터 저장의 업계 표준으로 부를 만하며, 일부 서버 소프트웨어인 lobste.rs도 SQLite에서 실행되고 있음
- 별도 프로세스로 실행되는 전통적인 RDBMS와 달리 라이브러리 형태의 RDBMS여서 소프트웨어를 자체 완결형으로 유지할 수 있음
- 일반 파일 형식처럼 사용자 정의 직렬화기와 파서를 작성할 필요가 없어, RDBMS와 파일 기반 저장 방식의 장점을 함께 제공함
- 그러나 데이터 무결성, 동시성, 성능에 영향을 주는 여러 기본값이 부적절하다는 문제가 남아 있음
외래 키를 강제하지 않아 생기는 무결성 문제
- 외래 키는 데이터베이스의 일관성을 유지하고 끊어진 참조를 방지하는 핵심 수단이지만, SQLite는 기본적으로 이를 강제하지 않음
- posts.user_id가 users.id를 참조하도록 선언해도 기본 설정에서는 다음 동작을 막지 못함
- 유효한 사용자 ID가 없는 게시물 생성
- 해당 사용자의 게시물을 남겨둔 채 사용자 삭제
- INTEGER PRIMARY KEY는 테이블의 ROWID 별칭이 되며, SQLite의 ROWID 할당 알고리듬은 일부 상황에서 ID를 재사용함
- Bob이 ID 1로 게시물을 작성한 뒤 계정만 삭제해도 외래 키 오류가 발생하지 않음
- 이후 Alice가 재사용된 ID 1을 받으면 Bob의 기존 게시물이 Alice에게 연결됨
- 조회 자체는 정상적으로 성공하므로 단순한 끊어진 참조보다 문제를 발견하기 어려움
- 연결마다 다음 프라그마를 활성화하면 외래 키를 강제할 수 있음
- 이를 처음부터 적용했다면 게시물을 남긴 사용자 삭제는 FOREIGN KEY constraint failed 오류로 중단됨
선언과 다른 자료형을 허용하는 일반 테이블
- SQLite 값은 NULL, INTEGER, REAL, TEXT, BLOB 가운데 하나지만, 일반 테이블의 열 선언은 저장 가능한 자료형을 엄격히 제한하지 않고 타입 친화도(type affinity) 를 결정함
- INTEGER 친화도 열에는 다음 저장 규칙이 적용됨
- 정수로 해석할 수 있는 TEXT는 INTEGER로 변환함
- 실수로 해석할 수 있는 TEXT는 배정밀도 부동소수점인 REAL로 변환함
- 그 밖의 값은 원래 자료형 그대로 저장함
- 다른 친화도에도 별도 변환 규칙이 있음
- BLOB 친화도는 값을 그대로 저장함
- TEXT 친화도는 BLOB, TEXT, NULL을 그대로 두고 숫자를 TEXT로 변환함
- REAL 친화도는 INTEGER 친화도처럼 동작하지만 정수를 REAL로 변환함
- 이 때문에 duration_sec INTEGER 열에도 숫자가 아닌 'Way too long, I mean come on' 같은 문자열을 저장할 수 있음
- 실제 프로젝트에서는 불리언용 숫자 1, 0 대신 문자열 '1', '0'이 기록되어 데이터를 정리하고 디버깅해야 했던 사례도 있었음
STRICT 테이블과 사용자 정의 타입의 충돌
- STRICT 테이블은 열에 잘못된 자료형을 넣으면 cannot store TEXT value in INTEGER column 같은 타입 오류를 발생시킴
-
모든 테이블을 전역적으로 엄격하게 만드는 프라그마는 없으므로, 각 CREATE TABLE 문에 strict를 빠짐없이 추가해야 함
-
SQLite는 유연한 타입이 유용하다는 입장이지만, STRICT 테이블에서도 ANY 자료형을 명시하면 모든 종류의 값을 저장하는 열을 만들 수 있음
- 임의 자료형을 허용할 열과 특정 자료형을 강제할 열을 명시적으로 구분할 수 있음
-
더 현실적인 호환성 문제는 STRICT 테이블이 자료형을 강제하는 데 그치지 않고 타입 지정자 해석 규칙까지 바꾼다는 점임
-
일반 테이블이 타입 이름을 해석하는 방식
- 일반 SQLite 테이블은 선언된 타입 문자열에 따라 친화도를 정함
- "INT"가 포함되면 INTEGER로 처리함
- "CHAR", "CLOB", "TEXT" 중 하나가 포함되면 TEXT가 됨
- "BLOB"이 포함되거나 타입을 생략하면 BLOB으로 처리함
- "REAL", "FLOA", "DOUB" 중 하나가 포함되면 REAL이 됨
- 어느 조건에도 해당하지 않으면 NUMERIC으로 처리함
- 느슨한 타입과 이 규칙을 결합하면 DATETIME, KEY_VALUE_SET, COLOR 같은 사용자 정의 타입 이름을 열에 붙일 수 있음
- 데이터베이스 커넥터나 래퍼는 타입 이름을 보고 자동으로 직렬화·역직렬화할 수 있음
- 별도 처리 기능이 없더라도 타입 이름 자체가 열의 예상 데이터를 문서화함
- STRICT를 그대로 기본값으로 바꾸면 이 활용법을 포기해야 하므로, 명시적인 사용자 정의 타입 별칭이 필요함
- 엄격한 테이블에서 KEY_VALUE_SET 같은 별칭을 사용할 수 있다면, 애플리케이션에서 파싱해야 하는 TEXT 열의 예상 데이터 패턴도 스키마에 기록할 수 있음
- 사용자 정의 타입에 CHECK 제약을 연결하는 기능도 유용함
- SQL:1999 표준에는 제약까지 지원하는 타입 별칭 문법인 CREATE DOMAIN 이 이미 있으므로, SQLite가 이 표준 문장을 지원하는 방식으로 구현할 수 있음
동시 쓰기와 SQLITE_BUSY
- SQLite는 여러 동시 읽기를 허용하지만, 쓰기는 한 번에 하나만 처리함
- 기본 설정에서는 두 프로세스가 동시에 쓰기 잠금을 얻으려 할 때 하나가 기다리지 않고 즉시 SQLITE_BUSY 오류를 받음
- 디스크 I/O를 수행하는 쓰기는 원래 느려질 수 있으므로, 일정 시간 잠금 해제를 기다리는 편이 더 자연스러움
- 실제 시스템이 간헐적으로 중단되어 직접 재시도 루프를 작성해야 했던 사례도 있음
- 다음 설정은 최대 5초 동안 잠금 획득을 재시도한 뒤에만 SQLITE_BUSY를 반환함
- 정상 운영에서는 모든 쓰기를 단일 프로세스, 가능하면 단일 스레드에 모으는 편이 적절하며 동시 쓰기가 빠를 수는 없음
- 다만 동시 쓰기가 필요한 정상적인 운영 사례도 있음
- sqlite3 명령줄 도구로 데이터베이스를 대화형으로 정리
- 별도 프런트엔드가 없는 드문 관리 작업용 스크립트 실행
- 이런 작업은 기본 설정 때문에 예상하지 못한 SQLITE_BUSY를 일으켜 실행 중인 소프트웨어를 중단시킬 수 있음
WAL과 성능 기본값
- SQLite는 올바르게 설정하면 PostgreSQL이나 MySQL 같은 대형 서버가 맡던 역할도 일부 수행할 만큼 빠를 수 있지만, 기본 성능은 좋지 않음
- 서버 환경의 세부 튜닝은 Optimizing SQLite for servers에서 더 확인할 수 있음
- 가장 중요한 기본값 문제는 Write-Ahead Log, 즉 WAL이 비활성화되어 있다는 점임
- WAL은 대부분의 상황에서 쓰기 속도를 크게 높임
- 다음 설정을 함께 사용하면 데이터 손상 위험 없이 디스크 동기화 횟수를 크게 줄일 수 있음
- synchronous의 구체적인 동작은 SQLite 프라그마 문서에 정리되어 있음
연도 기반 SQLite 에디션
- 기존 기본값을 지금 직접 변경하면 오래된 소프트웨어가 깨지고, 사용자가 향후 SQLite 업그레이드를 두려워하게 될 수 있음
- 하위 호환성을 유지하면서 개선된 기본값을 선택하도록 하나의 상위 프라그마(super pragma) 를 추가할 수 있음
- 2026 에디션은 최소한 다음 설정의 별칭으로 동작함
- 이 에디션에서는 새 테이블의 strict 모드도 기본값으로 적용함
- Rust editions처럼 기존 코드의 동작을 보존하면서 새로운 기본 동작을 명시적으로 선택하는 방식임
- JavaScript의 "use strict"; 같은 단일 모드와 달리, 연도 기반 에디션은 시대에 따라 합리적인 기본값 묶음을 바꿀 수 있음
- 예를 들어 Hctree의 WAL2가 2034년에 메인 브랜치에 들어간다면, 미래의 PRAGMA edition = 2034가 journal_mode = WAL2를 선택하도록 구성할 수 있음
- 에디션 체계는 기존 프로그램의 기본 동작을 깨지 않으면서도 SQLite가 과거 설정에 묶이지 않고 안전성과 성능 기본값을 발전시키는 절충안이 될 수 있음

6 days ago
8







English (US) ·