# SQLite에도 Rust 스타일의 에디션이 필요함

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

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=31473](https://news.hada.io/topic?id=31473)
- GeekNews Markdown: [https://news.hada.io/topic/31473.md](https://news.hada.io/topic/31473.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-16T09:06:58+09:00
- Updated: 2026-07-16T09:06:58+09:00
- Original source: [mort.coffee](https://mort.coffee/home/sqlite-editions/)
- Points: 3
- Comments: 2

## Topic Body

- SQLite는 로컬 저장소와 임베디드 프로젝트에 널리 쓰이는 자체 완결형 RDBMS지만, **하위 호환성** 때문에 안전성과 성능에 불리한 기본값을 유지하고 있음
- 기본적으로 **외래 키 제약을 강제하지 않고** `ROWID`를 재사용할 수 있어, 삭제된 사용자의 게시물이 같은 ID를 받은 다른 사용자에게 연결되는 데이터 무결성 문제가 생길 수 있음
- 일반 테이블은 선언된 열과 다른 자료형도 저장하며, **STRICT 테이블**은 이를 막지만 테이블마다 `strict`를 지정해야 하고 사용자 정의 타입 이름을 활용하던 방식과 충돌함
- 동시 쓰기에서 즉시 발생하는 `SQLITE_BUSY`, 비활성화된 **WAL**, 보수적인 동기화 설정은 각각 `busy_timeout`, `journal_mode`, `synchronous` 프라그마로 개선할 수 있음
- `PRAGMA edition = 2026` 같은 **연도 기반 에디션**으로 안전한 기본값 묶음을 선택하게 하면, 기존 동작을 깨지 않고 SQLite의 기본 설정을 계속 발전시킬 수 있음

---

### SQLite의 강점과 낡은 기본값
- SQLite는 임베디드 프로젝트의 데이터베이스이자 로컬 데이터 저장의 업계 표준으로 부를 만하며, 일부 서버 소프트웨어인 [lobste.rs도 SQLite에서 실행](https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)되고 있음
- 별도 프로세스로 실행되는 전통적인 RDBMS와 달리 **라이브러리 형태의 RDBMS**여서 소프트웨어를 자체 완결형으로 유지할 수 있음
- 일반 파일 형식처럼 사용자 정의 직렬화기와 파서를 작성할 필요가 없어, RDBMS와 파일 기반 저장 방식의 장점을 함께 제공함
- 그러나 데이터 무결성, 동시성, 성능에 영향을 주는 여러 **기본값이 부적절**하다는 문제가 남아 있음

### 외래 키를 강제하지 않아 생기는 무결성 문제
- 외래 키는 데이터베이스의 일관성을 유지하고 **끊어진 참조**를 방지하는 핵심 수단이지만, SQLite는 기본적으로 이를 강제하지 않음
- `posts.user_id`가 `users.id`를 참조하도록 선언해도 기본 설정에서는 다음 동작을 막지 못함
  - 유효한 사용자 ID가 없는 게시물 생성
  - 해당 사용자의 게시물을 남겨둔 채 사용자 삭제
- `INTEGER PRIMARY KEY`는 테이블의 `ROWID` 별칭이 되며, SQLite의 [`ROWID` 할당 알고리듬](https://sqlite.org/autoinc.html)은 일부 상황에서 ID를 재사용함
  - Bob이 ID `1`로 게시물을 작성한 뒤 계정만 삭제해도 외래 키 오류가 발생하지 않음
  - 이후 Alice가 재사용된 ID `1`을 받으면 Bob의 기존 게시물이 Alice에게 연결됨
  - 조회 자체는 정상적으로 성공하므로 단순한 끊어진 참조보다 문제를 발견하기 어려움
- 연결마다 다음 프라그마를 활성화하면 외래 키를 강제할 수 있음

```sql
PRAGMA foreign_keys = ON;
```

- 이를 처음부터 적용했다면 게시물을 남긴 사용자 삭제는 `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 테이블](https://sqlite.org/stricttables.html)은 열에 잘못된 자료형을 넣으면 `cannot store TEXT value in INTEGER column` 같은 **타입 오류**를 발생시킴

```sql
CREATE TABLE music (
  id INTEGER PRIMARY KEY,
  name TEXT,
  duration_sec INTEGER
) strict;
```

- 모든 테이블을 전역적으로 엄격하게 만드는 프라그마는 없으므로, 각 `CREATE TABLE` 문에 `strict`를 빠짐없이 추가해야 함
- SQLite는 [유연한 타입이 유용하다는 입장](https://sqlite.org/flextypegood.html)이지만, STRICT 테이블에서도 `ANY` 자료형을 명시하면 모든 종류의 값을 저장하는 열을 만들 수 있음
  - 임의 자료형을 허용할 열과 특정 자료형을 강제할 열을 명시적으로 구분할 수 있음
- 더 현실적인 호환성 문제는 STRICT 테이블이 자료형을 강제하는 데 그치지 않고 **타입 지정자 해석 규칙**까지 바꾼다는 점임
- ## 일반 테이블이 타입 이름을 해석하는 방식
  - 일반 SQLite 테이블은 [선언된 타입 문자열](https://sqlite.org/datatype3.html#determination_of_column_affinity)에 따라 친화도를 정함
  - `"INT"`가 포함되면 `INTEGER`로 처리함
  - `"CHAR"`, `"CLOB"`, `"TEXT"` 중 하나가 포함되면 `TEXT`가 됨
  - `"BLOB"`이 포함되거나 타입을 생략하면 `BLOB`으로 처리함
  - `"REAL"`, `"FLOA"`, `"DOUB"` 중 하나가 포함되면 `REAL`이 됨
  - 어느 조건에도 해당하지 않으면 `NUMERIC`으로 처리함
  - 느슨한 타입과 이 규칙을 결합하면 `DATETIME`, `KEY_VALUE_SET`, `COLOR` 같은 **사용자 정의 타입 이름**을 열에 붙일 수 있음
  - 데이터베이스 커넥터나 래퍼는 타입 이름을 보고 자동으로 직렬화·역직렬화할 수 있음
  - 별도 처리 기능이 없더라도 타입 이름 자체가 열의 예상 데이터를 문서화함
  - STRICT를 그대로 기본값으로 바꾸면 이 활용법을 포기해야 하므로, 명시적인 사용자 정의 타입 별칭이 필요함

  ```sql
  CREATE TYPE KEY_VALUE_SET = TEXT;
  ```

  - 엄격한 테이블에서 `KEY_VALUE_SET` 같은 별칭을 사용할 수 있다면, 애플리케이션에서 파싱해야 하는 `TEXT` 열의 예상 데이터 패턴도 스키마에 기록할 수 있음
  - 사용자 정의 타입에 [`CHECK` 제약](https://www.sqlite.org/lang_createtable.html#check_constraints)을 연결하는 기능도 유용함
  - SQL:1999 표준에는 제약까지 지원하는 타입 별칭 문법인 **`CREATE DOMAIN`** 이 이미 있으므로, SQLite가 이 표준 문장을 지원하는 방식으로 구현할 수 있음

### 동시 쓰기와 `SQLITE_BUSY`
- SQLite는 여러 동시 읽기를 허용하지만, 쓰기는 한 번에 하나만 처리함
- 기본 설정에서는 두 프로세스가 동시에 쓰기 잠금을 얻으려 할 때 하나가 기다리지 않고 즉시 **`SQLITE_BUSY` 오류**를 받음
  - 디스크 I/O를 수행하는 쓰기는 원래 느려질 수 있으므로, 일정 시간 잠금 해제를 기다리는 편이 더 자연스러움
  - 실제 시스템이 간헐적으로 중단되어 직접 재시도 루프를 작성해야 했던 사례도 있음
- 다음 설정은 최대 5초 동안 잠금 획득을 재시도한 뒤에만 `SQLITE_BUSY`를 반환함

```sql
PRAGMA busy_timeout = 5000;
```

- 정상 운영에서는 모든 쓰기를 단일 프로세스, 가능하면 **단일 스레드**에 모으는 편이 적절하며 동시 쓰기가 빠를 수는 없음
- 다만 동시 쓰기가 필요한 정상적인 운영 사례도 있음
  - `sqlite3` 명령줄 도구로 데이터베이스를 대화형으로 정리
  - 별도 프런트엔드가 없는 드문 관리 작업용 스크립트 실행
- 이런 작업은 기본 설정 때문에 예상하지 못한 `SQLITE_BUSY`를 일으켜 실행 중인 소프트웨어를 중단시킬 수 있음

### WAL과 성능 기본값
- SQLite는 올바르게 설정하면 PostgreSQL이나 MySQL 같은 대형 서버가 맡던 역할도 일부 수행할 만큼 빠를 수 있지만, 기본 성능은 좋지 않음
- 서버 환경의 세부 튜닝은 [Optimizing SQLite for servers](https://kerkour.com/sqlite-for-servers)에서 더 확인할 수 있음
- 가장 중요한 기본값 문제는 [Write-Ahead Log](https://sqlite.org/wal.html), 즉 **WAL이 비활성화**되어 있다는 점임

```sql
PRAGMA journal_mode = WAL;
```

- WAL은 대부분의 상황에서 쓰기 속도를 크게 높임
- 다음 설정을 함께 사용하면 데이터 손상 위험 없이 디스크 동기화 횟수를 크게 줄일 수 있음

```sql
PRAGMA synchronous = NORMAL;
```

- `synchronous`의 구체적인 동작은 [SQLite 프라그마 문서](https://sqlite.org/pragma.html#pragma_synchronous)에 정리되어 있음

### 연도 기반 SQLite 에디션
- 기존 기본값을 지금 직접 변경하면 오래된 소프트웨어가 깨지고, 사용자가 향후 SQLite 업그레이드를 두려워하게 될 수 있음
- 하위 호환성을 유지하면서 개선된 기본값을 선택하도록 하나의 **상위 프라그마(super pragma)** 를 추가할 수 있음

```sql
PRAGMA edition = 2026;
```

- `2026` 에디션은 최소한 다음 설정의 별칭으로 동작함

```sql
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
```

- 이 에디션에서는 새 테이블의 `strict` 모드도 기본값으로 적용함
- [Rust editions](https://doc.rust-lang.org/cargo/reference/manifest.html#the-edition-field)처럼 기존 코드의 동작을 보존하면서 새로운 기본 동작을 명시적으로 선택하는 방식임
- JavaScript의 [`"use strict";`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) 같은 단일 모드와 달리, 연도 기반 에디션은 시대에 따라 합리적인 기본값 묶음을 바꿀 수 있음
- 예를 들어 [Hctree의 WAL2](https://sqlite.org/hctree/doc/wal2/doc/wal2.md)가 2034년에 메인 브랜치에 들어간다면, 미래의 `PRAGMA edition = 2034`가 `journal_mode = WAL2`를 선택하도록 구성할 수 있음
- 에디션 체계는 기존 프로그램의 기본 동작을 깨지 않으면서도 SQLite가 과거 설정에 묶이지 않고 **안전성과 성능 기본값을 발전시키는 절충안**이 될 수 있음

## Comments



### Comment 61895

- Author: tomskang
- Created: 2026-07-16T10:12:06+09:00
- Points: 3

이론은 좋아 보이지만 호환성 충돌 및 오버헤드를 생각하면 손해일 것 같아요.  
SQLite 는 이지스 같은 임베디드 환경에서 동작하기 위해 탄생한 DBMS 인데요,  
 void** 뻥뻥 때리고 __weak__ 선언후에 함수 덮어씌우기 하는게 임베디드 세상이죠. 그런세상에서 저걸 굳이? 싶은 생각이 드네요

### Comment 61883

- Author: neo
- Created: 2026-07-16T09:06:59+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/2nry82/sqlite_should_have_rust_style_editions)   
- 어제 lobste.rs가 SQLite로 전환했다는 소식(https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite)을 보고 이 글을 썼음  
  당시 대화를 읽으며 **SQLite 기본값의 사소한 불편들**이 떠올라 블로그 글로 정리함  
  - 나도 그 대화를 읽고 같은 결론에 도달함  
    여력이 있다면 이 글을 **SQLite 포럼**에도 공유할 만함 : https://sqlite.org/forum/forum  
  
- SQL에서 **`CREATE TYPE`** 은 복합 타입을 만들 때 사용하며, Postgres는 여기에 범위, 열거, 배열, 상당히 복잡한 기반 타입까지 지원함  
  원하는 기능은 **`CREATE DOMAIN`** 에 가깝다. 기반 구체 타입으로 새타입(newtype)을 만들고 기본값과 제약 조건도 지정할 수 있어, 여러 테이블에서 같은 `CHECK` 제약 조건을 반복하는 스키마가 훨씬 간결해짐. 예를 들면 `create domain mything as text;` 또는 `create domain mything as integer not null check (VALUE < 5);`처럼 쓸 수 있음  
  - 기존 **타입 별칭 기능**이 있는지 먼저 확인했어야 했음. 글에 관련 내용을 추가하겠음  
  
- **`busy_timeout`만으로는** 즉시 발생하는 `SQLITE_BUSY` 오류를 막을 수 없다는 꽤 위험한 함정도 다룰 필요가 있음  
  읽기 전용 쿼리로 트랜잭션을 시작한 뒤 쓰기 쿼리를 실행해 보유한 잠금을 승격하려 할 때 다른 쓰기 작업이 있으면, `busy_timeout` 설정과 무관하게 즉시 실패함. 해결하려면 **`begin immediate`** 를 사용해야 함  
  - 실제 버그가 아니라는 건 알지만, 솔직히 거의 **버그처럼 느껴지는 동작**임  
  
- 제안한 에디션에 묶인 설정 중 일부는 SQLite 모델에서 **연결 속성**이고, 나머지는 데이터베이스 파일이나 개별 테이블의 속성이라는 점을 고려해야 함  
  `PRAGMA journal_mode = WAL`은 데이터베이스 파일에 계속 유지되지만 `PRAGMA foreign_keys = ON`은 연결마다 매번 설정해야 함. `STRICT`는 개별 테이블의 속성이어서 한 데이터베이스 안에 엄격한 테이블과 그렇지 않은 테이블을 섞을 수도 있음. 따라서 이 **통합 PRAGMA**를 데이터베이스 파일과 연결 중 어디의 속성으로 정의할지 정해야 함. 구버전 라이브러리가 자신이 모르는 기능이 포함된 파일을 수정해 망가뜨리지 않도록 경고하는 내장 장치가 있는지도 궁금  
  
- **에디션 방식**은 기본적으로 하위 호환성을 유지하려는 SQLite 개발진의 목표와, 최신 권장 설정을 원하는 사용자를 위한 덜 혼란스러운 기본값 사이의 좋은 절충안으로 보임  
  
- 최근 **RocksDB**에 푹 빠졌음. SQLite처럼 내장형 데이터베이스 라이브러리지만 SQL이 없다는 점이 오히려 큰 장점이라고 봄  
  모든 작업을 SQL로 표현해야 하는 제약은 데이터베이스를 다루는 데 방해만 된다고 느낌  
  - RocksDB는 **키-값 데이터베이스**이고 SQLite는 **관계형 데이터베이스**라서, 서로 다른 목적을 위해 만들어진 별개의 개념이라고 좀  
    lobste.rs처럼 SQL을 사용하는 시스템을 RocksDB나 다른 키-값 데이터베이스로 어떻게 옮길지 상상하기 어려움  
  
- `busy_timeout`을 설정하는 것이 해결책이라는 데 동의하지 않음. SQLite는 **단일 쓰기 스레드**를 두고 서로 무관한 논리적 트랜잭션을 일괄 처리하면 성능이 매우 뛰어남  
  구체적인 방식은 https://andersmurphy.com/2025/12/02/100000-tps-over-a-billion-rows-the-unreasonable-effectiveness-of-sqlite.html 에 설명돼 있음  
  - 일반적인 작업 부하에서는 **쓰기 작업을 하나로 제한**하고, 여러 쓰기 작업이 상시 경쟁하는 구조를 만들지 않는 편이 맞음  
    하지만 정리 작업을 위해 데이터베이스를 직접 수정하거나, 별도 관리 화면까지 만들 필요는 없는 간헐적인 관리 작업을 스크립트로 실행하는 일도 흔함. 이때 운 나쁘게 실행 시점이 겹쳤다는 이유로 서버나 애플리케이션이 무작위로 중단될 수 있다는 건 좋지 않음
