진가를 발휘하는 두 가지 제약조건
스키마를 대충 짜다 생기는 버그는 거의 두 가지로 압축됩니다. 아무도 예상하지 못한 자리에 NULL이 들어가 있거나, 애플리케이션이 당연히 있을 거라 믿었던 값이 비어 있거나. SQLite의 NOT NULL 제약조건과 DEFAULT 값은 이 두 문제를 깔끔하게 해결해 주는데, 추가하는 비용도 거의 들지 않습니다.
컬럼 하나는 필수값이고 대체값이 없습니다. 나머지 두 개는 기본값이 있죠. INSERT 할 때 email만 넘겨줬고, 나머지는 SQLite가 알아서 채웠습니다. 핵심 동작은 이 예제 하나로 끝입니다 — 이 페이지의 나머지 내용은 모두 예외 상황과 디테일에 관한 이야기예요.
SQLite NOT NULL은 "NULL 절대 허용 안 함"
NOT NULL 제약조건은 이름 그대로 동작합니다. 컬럼에 NULL을 넣으려는 시도는 무조건 실패해요. 기본값이 없는 컬럼을 INSERT에서 빼먹든, 아니면 NULL을 직접 명시하든 결과는 똑같습니다:
오류 메시지는 다음과 같이 출력됩니다:
Runtime error: NOT NULL constraint failed: posts.title
NULL을 직접 전달해도 결과는 동일합니다:
INSERT INTO posts (id, title) VALUES (1, NULL);
-- 런타임 오류: NOT NULL constraint failed: posts.title
약속은 명확합니다. 어떤 컬럼이 논리적으로 필수라면 NOT NULL을 붙이세요. 그 순간부터 한 부류의 버그가 통째로 사라집니다 — 어떤 애플리케이션 코드도 NULL을 데이터베이스에 몰래 집어넣을 수 없으니까요.
호출자가 값을 주지 않을 때 채워주는 SQLite DEFAULT 값
DEFAULT는 INSERT 문에서 해당 컬럼을 아예 언급하지 않았을 때만 동작합니다. 명시적으로 넘긴 NULL까지 구해주지는 않습니다:
첫 번째 INSERT는 기본값에 의존하고, 두 번째는 그 값을 직접 덮어씁니다. 만약 INSERT INTO tasks (title, status) VALUES ('x', NULL)처럼 썼다면 NOT NULL constraint failed 에러가 났을 겁니다. 컬럼을 명시한 순간 기본값은 작동하지 않으니까요.
여기서 꼭 기억해 둘 멘탈 모델이 있습니다. DEFAULT는 빠진 컬럼을 채워 줄 뿐이고, NOT NULL은 어떤 경로로 들어오든 NULL을 거부한다는 점입니다. 둘은 서로 독립적인 기능이지만, 함께 쓰면 궁합이 아주 좋습니다.
SQLite DEFAULT 값에는 표현식도 쓸 수 있다
기본값으로는 보통 리터럴을 많이 씁니다 (DEFAULT 0, DEFAULT '', DEFAULT 'pending' 같은 식이죠). 하지만 SQLite는 괄호로 감싼 표현식도 허용합니다. 행이 생성된 시각을 자동으로 찍거나, 랜덤 ID를 생성하고 싶을 때 바로 이 방식을 사용합니다.
몇 가지 짚고 넘어갈 점:
- 이 표현식은 테이블을 만들 때 한 번 평가되는 게 아니라 매 INSERT마다 평가됩니다. 즉, 행마다 자기만의 타임스탬프와 토큰 값을 갖게 됩니다.
- 괄호 없이 쓸 수 있는 특수 키워드는
CURRENT_TIMESTAMP,CURRENT_DATE,CURRENT_TIME이렇게 세 개뿐입니다. 나머지는 모두 괄호로 감싸야 합니다. - 표현식 안에서 다른 컬럼이나 서브쿼리를 참조할 수는 없습니다. 자기 자신만으로 완결돼야 합니다.
값을 안 넣어도 되지만 넣을 땐 자동으로 찍히게 하고 싶다면 NOT NULL을 빼고 default만 남기면 됩니다. 반대로 값이 반드시 있어야 하고 동시에 자동으로 채워지길 원한다면 둘 다 같이 쓰면 됩니다.
DEFAULT NULL도 유효한 문법입니다 (그리고 의도적으로 쓸 때도 있죠)
DEFAULT NULL이라고 쓰는 건 default를 아예 안 쓴 것과 결과적으로 똑같습니다. 값을 안 주면 그 컬럼은 NULL이 됩니다. 그래도 굳이 쓰는 이유는, "이 컬럼의 기본 상태는 값 없음이다"라는 의도를 스키마에 명시적으로 드러내고 싶을 때입니다:
bio와 avatar는 사실상 똑같이 동작합니다. bio에 붙은 DEFAULT NULL은 코드로 작성한 주석에 가까워요. bio가 비어 있는 게 실수가 아니라 정상적인 상태라는 걸 스키마를 읽는 사람에게 알려주는 역할을 합니다.
기존 테이블에 NOT NULL 추가하기
여기서부터 좀 까다로워집니다. SQLite의 ALTER TABLE은 의도적으로 기능이 제한돼 있어서, Postgres처럼 ALTER COLUMN ... SET NOT NULL 같은 명령을 쓸 수 없습니다. 할 수 있는 일은 해당 컬럼이 이미 존재하는지 여부에 따라 달라집니다.
새로 추가하는 컬럼이라면 ADD COLUMN ... NOT NULL이 먹히긴 하는데, 반드시 기본값(default)을 함께 지정해야 합니다. 그렇지 않으면 기존 행들이 갑자기 NOT NULL 컬럼에 NULL을 갖게 되는 모순이 생기기 때문이죠:
같은 쿼리를 DEFAULT 값 없이 실행하면 다음과 같은 에러가 발생합니다:
ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- 실행 오류: 기본값이 NULL인 NOT NULL 컬럼은 추가할 수 없습니다
기존 컬럼은 제자리에서 바꿀 수 없습니다. 표준적인 방법은 이른바 "테이블 재구성" 절차인데요, 원하는 제약조건을 갖춘 새 테이블을 만들고 → 데이터를 옮기고 → 기존 테이블을 삭제한 뒤 → 새 테이블의 이름을 바꾸는 식으로 진행합니다. 자세한 내용은 drop-and-alter-table 페이지에서 다루겠지만, 이 한계는 실제로 존재하니 스키마를 설계할 때 미리 염두에 두세요.
실전에서 자주 쓰는 조합
운영 환경의 테이블 대부분은 "애플리케이션이 당연히 그럴 것이라고 기대하는 값"을 표현하기 위해 두 제약조건을 함께 사용합니다.
스키마를 위에서 아래로 한번 훑어보면, 코드를 한 줄도 보지 않고도 이 애플리케이션이 무슨 일을 하는지 짐작할 수 있습니다. customer는 필수이고 기본값도 없으니, 호출하는 쪽에서 주문이 누구 것인지 반드시 알아야 합니다. 금액, 통화, 상태에는 모두 적절한 기본값이 지정돼 있어서, 가장 단순한 INSERT만 날려도 일관된 행이 만들어집니다. notes는 선택 항목이고요. created_at은 데이터베이스가 알아서 채워주는데, 사실 이 값은 딱 그 자리에서만 채워져야 합니다.
이게 바로 SQLite 제약조건의 가치입니다. 머릿속에만 있던 가정들을 데이터베이스가 직접 강제하는 규칙으로 바꿔주죠.
자주 빠지는 함정
실무에서 사람들이 자주 걸려 넘어지는 지점들을 정리해 봤습니다.
- 명시적
NULL은DEFAULT를 덮어씁니다.INSERT INTO t (col) VALUES (NULL)은 기본값을 쓰지 않습니다. 컬럼 목록에서 해당 컬럼을 아예 빼야 기본값이 적용됩니다. - 표현식 기본값은 괄호로 감싸야 합니다.
DEFAULT CURRENT_TIMESTAMP는 됩니다(세 개의 특수 키워드 중 하나니까요). 하지만DEFAULT lower(hex(randomblob(8)))는 안 됩니다. 이렇게 괄호로 감싸야 해요:DEFAULT (lower(hex(randomblob(8)))). NOT NULL과 빈 문자열은 다릅니다.''은 유효한TEXT값이라 NOT NULL 제약을 위반하지 않습니다. 빈 문자열까지 막고 싶다면 그건CHECK의 영역입니다(다음 페이지에서 다룹니다).ADD COLUMN ... NOT NULL에는NULL이 아닌DEFAULT가 반드시 있어야 합니다. 기본값 없이 NOT NULL 컬럼을 추가하려고 하면 SQLite가 거부합니다.
다음: CHECK 제약조건
NOT NULL과 DEFAULT는 "반드시 존재해야 한다"와 "없으면 채워 넣어라"까지를 커버합니다. 그 다음 단계의 검증, 즉 "양수여야 한다", "이 값들 중 하나여야 한다", "종료일은 시작일 이후여야 한다" 같은 규칙은 SQLite의 CHECK 제약조건이 담당합니다. 모든 행이 만족해야 할 임의의 불리언 표현식을 자유롭게 작성할 수 있죠. 다음 페이지에서 이어집니다.
자주 묻는 질문
SQLite에서 특정 컬럼을 필수값으로 만들려면 어떻게 하나요?
컬럼 정의 뒤에 NOT NULL을 붙이면 됩니다. 예를 들면 email TEXT NOT NULL처럼요. 이 컬럼을 NULL로 두는 INSERT나 UPDATE는 모두 NOT NULL constraint failed 오류로 거부됩니다. 호출 측에서 값을 안 넘겼을 때 대체값이 필요하다면 DEFAULT와 함께 쓰는 걸 추천합니다.
SQLite의 DEFAULT 값은 어떻게 동작하나요?
DEFAULT <값>은 INSERT 시 해당 컬럼을 명시하지 않았을 때 사용할 값을 지정합니다. 리터럴(DEFAULT 0, DEFAULT 'pending'), NULL, 또는 괄호로 감싼 표현식(DEFAULT (CURRENT_TIMESTAMP), DEFAULT (lower(hex(randomblob(8)))))까지 모두 가능합니다. 표현식 기본값은 INSERT가 일어날 때마다 새로 평가된다는 점이 포인트예요.
INSERT할 때 'NOT NULL constraint failed' 오류가 왜 나나요?
DEFAULT가 없는 NOT NULL 컬럼에 값을 넣지 않은 채로 행을 삽입하려고 했기 때문입니다. 해결 방법은 세 가지예요: INSERT 문에 해당 컬럼을 포함시키거나, 컬럼에 DEFAULT를 지정하거나, 제약을 푸는 거죠. 참고로 NULL을 명시적으로 넘겨도 똑같이 실패합니다. NOT NULL은 NULL이 어디서 왔든 거부하니까요.
이미 존재하는 컬럼에 NOT NULL을 추가할 수 있나요?
직접은 안 됩니다. SQLite에는 ALTER TABLE ... ALTER COLUMN이 없거든요. 대안은 두 가지입니다. 첫째, NOT NULL DEFAULT <값> 형태로 새 컬럼을 추가하는 방법(기존 행 때문에 DEFAULT가 필수). 둘째, 테이블을 재생성하는 방법으로, 제약이 적용된 새 테이블을 만들고 데이터를 복사한 뒤 원본을 DROP하고 새 테이블 이름을 바꾸면 됩니다.