STRICT 테이블이 왜 생겼을까
SQLite는 기본적으로 타입을 굉장히 느슨하게 다루는 걸로 유명합니다. 컬럼을 INTEGER로 선언해놓고 "hello"라는 문자열을 집어넣어도, SQLite는 별 불평 없이 그대로 저장해 버리죠. 이런 유연함은 90년대에 의도적으로 내린 설계 결정이지만, Postgres나 MySQL을 쓰다 넘어온 사람 입장에서는 당황스럽고, 무엇보다 버그를 숨기기 딱 좋습니다.
SQLite 3.37부터 도입된 STRICT 테이블은 바로 이 문제를 해결합니다. 테이블 단위로 옵트인 방식이고, 한 번 켜두면 그 뒤로는 컬럼 타입이 선언한 그대로 강제됩니다.
STRICT 키워드는 닫는 괄호 뒤에 붙입니다. 나머지는 평범한 CREATE TABLE 문과 똑같죠. 차이는 컬럼에 엉뚱한 타입의 값을 넣어보는 순간 바로 드러납니다.
STRICT 테이블이 실제로 강제하는 것
일반 테이블에서는 타입 어피니티(type affinity)가 값을 선언된 타입으로 변환해 보고, 안 되면 그냥 들어온 그대로 저장해 버립니다. 반면 STRICT 테이블에서는 타입이 맞지 않으면 곧바로 에러가 납니다.
같은 동작을 STRICT가 아닌 일반 테이블에서 시도해 보면 세 번째 INSERT가 그냥 통과해 버립니다. INTEGER라고 선언한 컬럼에 SQLite가 태연하게 문자열 'oops'를 집어넣는 거죠. 그러고는 몇 달 뒤, 어느 집계 쿼리가 이상한 값을 뱉기 시작하고 여러분은 오후 내내 원인을 추적하느라 허덕이게 됩니다. STRICT 테이블을 쓰면 이런 실패가 INSERT 시점에 바로 터지기 때문에, 그 자리에서 바로 잡을 수 있습니다.
이때 보게 되는 에러는 다음과 같습니다:
ランタイムエラー: TEXT 値を INTEGER カラム accounts.balance に格納できません
짧고 분명하게, 그냥 무시할 수 없는 동작이죠.
허용되는 다섯 가지 타입
STRICT 테이블에서는 딱 다섯 가지 타입 이름만 받습니다:
INTEGER— 정수.REAL— 부동소수점 숫자.TEXT— 문자열.BLOB— 바이트 원본 데이터.ANY— 어떤 타입이든 가능, 변환 없음.
이게 전부입니다. 평소 SQLite가 너그럽게 받아주던 별칭들 — VARCHAR(255), DOUBLE, BOOLEAN, DATETIME, INT — 은 STRICT 테이블 안에서는 모두 에러를 냅니다:
ランタイムエラー: TEXT 値を INTEGER カラム accounts.balance に格納できません
解析エラー: bad.name のデータ型が不明です: "VARCHAR(255)"
다섯 가지 표준 이름 중 하나로 바꿔주면 해결됩니다. VARCHAR(255)는 TEXT로, DATETIME도 TEXT로 (어차피 SQLite는 날짜를 ISO 문자열로 저장합니다), BOOLEAN은 INTEGER로 (0과 1 사용) 바꾸면 됩니다.
ANY 컬럼: 타입 강제를 우회하는 비상구
ANY는 STRICT 테이블에서도 서로 다른 타입의 값을 한 컬럼에 담을 수 있게 해주는 유일한 타입입니다. 키/값 테이블의 범용 value 컬럼처럼, 여러 타입을 섞어 저장해야 할 때 유용하죠:
ANY는 STRICT 테이블 안에서 특별하게 동작합니다. 다른 곳에서라면 일어났을 타입 강제 변환 없이 값을 그대로 저장하죠. '100' 문자열은 문자열로, 100 정수는 정수로 남습니다. 위 쿼리의 typeof() 결과가 이를 증명합니다.
일반(non-STRICT) 테이블이었다면 ANY 어피니티 컬럼은 숫자처럼 보이는 문자열을 숫자로 바꿔버렸을 겁니다. 반면 STRICT는 원본 타입을 그대로 보존합니다.
STRICT 테이블과 PRIMARY KEY
미묘하지만 중요한 차이가 하나 있습니다. 일반 테이블에서 INTEGER PRIMARY KEY는 특별 취급되어 rowid의 별칭이 되고 정수만 허용합니다. 반면 그 외의 기본 키 선언은 타입 검사가 훨씬 느슨하죠.
STRICT 테이블에서는 기본 키 여부와 상관없이 컬럼 타입이 그대로 강제됩니다:
두 번째 INSERT는 실패합니다. 일반 테이블이었다면 TEXT 기본 키 컬럼에 42가 조용히 저장됐겠지만, STRICT 테이블에서는 곧바로 오류로 알려줍니다.
STRICT 테이블과 일반 테이블 함께 쓰기
STRICT는 데이터베이스 단위가 아니라 테이블 단위로 적용됩니다. 같은 파일 안에서 users는 STRICT로, events는 일반 테이블로 두는 것도 얼마든지 가능합니다. 두 테이블 사이의 외래 키도 평소와 똑같이 잘 동작합니다.
events 테이블은 STRICT도 없고 payload에 선언된 타입도 없어서, 뭘 넣든 다 받아줍니다. 가끔 유용하긴 하지만 기본값으로 쓰기엔 위험하죠. 타입 없는 저장은 정말로 잡탕 컬럼이 필요한 경우에만 아껴 쓰세요.
STRICT 테이블을 언제 쓸까
새로 만드는 스키마라면 답은 "거의 항상 쓰자"입니다. 비용이라곤 테이블당 키워드 하나, 그리고 정해진 다섯 가지 타입 이름을 기억하는 정도가 전부예요. 대신 평소라면 데이터 어딘가에 조용히 숨어 있을 버그가 문제를 일으킨 그 INSERT 시점에 바로 튀어나옵니다.
다음과 같은 경우라면 STRICT는 건너뛰는 게 좋습니다:
- 기존 스키마가 느슨한 타입에 의존하고 있는 오래된 SQLite DB를 유지보수하는 경우.
- SQLite 3.37(2021년 10월) 이전 버전을 타겟으로 하는 경우 — 그 버전에는 키워드 자체가 없습니다.
- 실제로 한 컬럼에 여러 타입을 섞어 담고 싶은 경우. 이때도 일반 테이블보다는
STRICT+ANY컬럼 조합이 낫습니다. 나머지 컬럼은 여전히 타입 검사가 적용되니까요.
일반 테이블을 STRICT로 바꿀 때 쓰는 짧은 체크리스트:
VARCHAR,CHAR,NVARCHAR→TEXT로 교체.DOUBLE,FLOAT,NUMERIC→REAL로 교체.BOOLEAN,BIT,TINYINT→INTEGER로 교체.DATETIME,TIMESTAMP,DATE→TEXT로 (유닉스 타임스탬프로 저장한다면INTEGER).- 닫는 괄호 뒤에
STRICT추가.
다음 주제: 기본 키
STRICT 테이블은 컬럼이 데이터를 어떻게 담을지를 조여줍니다. 다음으로 조일 만한 것은 어떤 컬럼이 각 행을 식별하느냐인데요. SQLite의 기본 키는 (특히 INTEGER PRIMARY KEY와 rowid 주변에) 알아둘 만한 특이점이 몇 가지 있어서, 실제 스키마를 설계하기 전에 짚고 넘어가면 좋습니다.
자주 묻는 질문
SQLite의 STRICT 테이블은 뭔가요?
선언한 컬럼 타입을 그대로 강제하는 테이블입니다. 예를 들어 컬럼을 INTEGER로 선언했다면, 정수나 NULL이 아닌 값은 SQLite가 거부해 버립니다. 사용 방법은 간단해서, CREATE TABLE의 닫는 괄호 뒤에 STRICT 키워드만 붙이면 됩니다. 이 키워드를 빼면 SQLite는 평소처럼 타입 어피니티 규칙을 따르는데, 이때는 변환 가능한 값은 알아서 변환하고 안 되면 그냥 들어온 형태로 저장해 버립니다.
STRICT 테이블에서 쓸 수 있는 타입은 뭐가 있나요?
딱 5가지입니다. INTEGER, REAL, TEXT, BLOB, 그리고 ANY. 일반 테이블에서는 잘 통하던 VARCHAR, DOUBLE, BOOLEAN, DATETIME 같은 별칭들은 STRICT 테이블에서 모두 에러를 냅니다. ANY 컬럼은 일종의 탈출구로, 어떤 타입의 값이든 변환 없이 그대로 받아 줍니다.
새로 만드는 SQLite DB에는 STRICT 테이블을 쓰는 게 좋을까요?
대부분의 경우 그렇습니다. 일반 테이블이 조용히 넘어가는 버그들 — INTEGER 컬럼에 문자열이 슬쩍 들어가거나, REAL에 직렬화된 리스트가 그대로 박히는 상황 — 을 STRICT 테이블은 바로 잡아 줍니다. 대신 테이블마다 키워드 하나를 더 써야 하고, 별칭 타입명을 못 쓴다는 점은 감수해야 합니다. SQLite 3.37(2021년)부터 사용 가능합니다.