백엔드 데이터 인프라 23편. 제약 깊이 — 복합 CHECK 표현식·DEFERRABLE·EXCLUDE·복합 키와 운영 함정 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 23편이에요. 9편 CREATE TABLE ·15편 외래 키 에서 기본 제약을 다뤘죠. 이번 23편은 PG가 가진 더 강력한 제약 도구들 — CHECK 표현식·DEFERRABLE·EXCLUDE.
제약의 5가지 종류
9편 의 6개 제약을 분류:
| 제약 | 의미 | 분류 |
|---|---|---|
| NOT NULL | 값 필수 | 컬럼 |
| UNIQUE | 중복 X | 컬럼·테이블 |
| PRIMARY KEY | NOT NULL + UNIQUE + 인덱스 | 테이블 |
| FOREIGN KEY | 참조 무결성 | 테이블 |
| CHECK | 조건 검증 | 컬럼·테이블 |
| EXCLUDE | 범위·조건 충돌 방지 | 테이블 |
EXCLUDE는 PG 특별 — 다른 RDBMS엔 없음. 23편 후반에 깊이.
CHECK 제약 깊이
컬럼 수준
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
price INTEGER CHECK (price >= 0),
stock INTEGER CHECK (stock >= 0),
grade CHAR(1) CHECK (grade IN ('A', 'B', 'C'))
);
한 컬럼에 대한 단순 검증.
테이블 수준 — 여러 컬럼 조합
CREATE TABLE date_ranges (
id BIGSERIAL PRIMARY KEY,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CHECK (end_date >= start_date)
);
여러 컬럼 사이 관계 — 컬럼 수준 X, 테이블 수준 CHECK.
함수 호출 CHECK
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT CHECK (email ~ '^[^@]+@[^@]+\.[^@]+$')
);
정규식·내장 함수·사용자 함수 모두 가능. 단 — STABLE 또는 IMMUTABLE 함수만 (성능·복제 위함).
NOT VALID — 사후 추가
15편 외래 키 에서 다룬 NOT VALID 패턴이 CHECK 에도.
-- 1. 새 데이터에만 검증
ALTER TABLE products ADD CONSTRAINT positive_price
CHECK (price >= 0) NOT VALID;
-- 2. 백그라운드 검증
ALTER TABLE products VALIDATE CONSTRAINT positive_price;
큰 테이블에 사후 제약 추가 = 다운타임 0.
명명
CONSTRAINT chk_products_price CHECK (price >= 0)
명시적 이름 박으면 — ERROR 메시지·DROP 시 명확. 큰 시스템 운영에 표준.
CHECK vs 트리거
CHECK 가능 = 한 행 한 시점 검증. 트리거 필요 = 다른 행·테이블 참조하는 검증.
-- CHECK 불가능 (다른 행 참조)
-- "같은 시간에 같은 사용자가 주문 2개 못 함"
-- 트리거로 구현
CREATE FUNCTION check_duplicate_order()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (SELECT 1 FROM orders
WHERE user_id = NEW.user_id
AND created_at = NEW.created_at
AND id <> NEW.id) THEN
RAISE EXCEPTION '중복 주문';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_check_duplicate
BEFORE INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION check_duplicate_order();
DEFERRABLE — 트랜잭션 끝에 검증
15편 에서 짧게 본 옵션. 깊이.
즉시 vs 지연
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
DEFERRABLE INITIALLY DEFERRED -- 기본 = 지연
-- 또는 DEFERRABLE INITIALLY IMMEDIATE -- 즉시 (DEFERRABLE 단어만 박힌 의미)
기본:
- NOT DEFERRABLE (기본) = 매 SQL마다 즉시 검증
- DEFERRABLE INITIALLY IMMEDIATE = 즉시지만 SET CONSTRAINTS로 지연 가능
- DEFERRABLE INITIALLY DEFERRED = 트랜잭션 끝까지 미룸
순환 참조 시나리오
CREATE TABLE departments (
id BIGINT PRIMARY KEY,
manager_id BIGINT
);
CREATE TABLE employees (
id BIGINT PRIMARY KEY,
department_id BIGINT REFERENCES departments(id) DEFERRABLE
);
ALTER TABLE departments ADD CONSTRAINT fk_manager
FOREIGN KEY (manager_id) REFERENCES employees(id) DEFERRABLE;
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
INSERT INTO departments (id, manager_id) VALUES (1, 100); -- 100 없음 — OK (지연)
INSERT INTO employees (id, department_id) VALUES (100, 1); -- 둘 다 만족
COMMIT; -- 여기서 한 번에 검증
순환 의존성 처리에 필수.
비싼 검증 일괄
-- 한 트랜잭션 안 여러 UPDATE·INSERT, 검증은 한 번만
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
UPDATE orders SET status = 'PAID' WHERE ...;
INSERT INTO order_logs SELECT id, 'PAID' FROM orders WHERE ...;
UPDATE inventory SET stock = stock - 1 WHERE ...;
COMMIT;
검증 N번 → 1번으로. 큰 배치 작업에 유용.
EXCLUDE 제약 — PG 특별
UNIQUE 가 "같은 값 안 됨" 이라면, EXCLUDE = "겹치는 범위·조건 안 됨".
CREATE EXTENSION btree_gist;
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
room_id BIGINT NOT NULL,
period TSRANGE NOT NULL,
EXCLUDE USING GIST (room_id WITH =, period WITH &&)
);
room_id WITH == 같은 방period WITH &&= 겹치는 기간
같은 방의 겹치는 시간 예약 = 거부.
INSERT INTO bookings (room_id, period)
VALUES (1, '[2026-05-17 10:00, 2026-05-17 12:00)'); -- OK
INSERT INTO bookings (room_id, period)
VALUES (1, '[2026-05-17 11:00, 2026-05-17 13:00)'); -- ERROR! 겹침
예약·일정·재고 시스템에 매우 강력.
EXCLUDE WHERE — 조건부
EXCLUDE USING GIST (room_id WITH =, period WITH &&)
WHERE (canceled IS NOT TRUE)
취소된 예약은 검사 안 함.
복합 키·복합 UNIQUE
CREATE TABLE order_items (
order_id BIGINT,
line_no INTEGER,
product_id BIGINT NOT NULL,
PRIMARY KEY (order_id, line_no)
);
-- 또는 unique
CREATE TABLE user_tags (
user_id BIGINT,
tag TEXT,
UNIQUE (user_id, tag)
);
다중 컬럼 키 — JPA @EmbeddedId 매핑.
컬럼 순서가 중요
UNIQUE (a, b) -- (a, b) 조합 unique. a 단독 unique X
복합 인덱스 동작과 같음 — a 만 조회는 인덱스 활용, b 만 조회는 활용 X.
제약 관리
확인
\d users -- psql 메타 — 제약 함께 표시
-- 또는 시스템 카탈로그
SELECT
conname,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'users'::regclass;
변경
ALTER TABLE users
DROP CONSTRAINT chk_email,
ADD CONSTRAINT chk_email CHECK (email ~ '...');
DROP + ADD 가 표준. 직접 ALTER 불가.
일시 비활성화
ALTER TABLE users DISABLE TRIGGER ALL; -- 모든 트리거 (제약 트리거 포함)
ALTER TABLE users ENABLE TRIGGER ALL;
위험 — 데이터 무결성 깨질 수 있음. 데이터 이관·복구 시만.
Spring·JPA 와의 매핑
자바 백엔드 입문 35편 Bean Validation 의 @Valid·@NotNull·@Email 등이 — DB CHECK 제약과 대응.
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true)
@Email
private String email;
@Min(0)
private Integer age;
}
JPA가 ddl-auto: update 모드에서 — NOT NULL·UNIQUE를 자동 생성. 단 — @Email·@Min 은 DB CHECK 자동 생성 X. Hibernate Validator 가 앱 단에서 검증.
룰: 비즈니스 룰 검증은 양쪽 다 — 앱(즉시 피드백) + DB(최후 방어선).
함정 5가지
(1) CHECK 에 VOLATILE 함수
CHECK (created_at = NOW()) -- ❌ NOW()는 VOLATILE
CHECK 는 IMMUTABLE·STABLE 함수만. VOLATILE 박으면 — 복제·재생성 시 문제.
(2) DEFERRABLE 안 박고 SET CONSTRAINTS
SET CONSTRAINTS ALL DEFERRED; -- DEFERRABLE 없는 제약은 영향 X
처음부터 DEFERRABLE 박아야 SET 가 동작.
(3) EXCLUDE + btree_gist 누락
EXCLUDE USING GIST (room_id WITH =, ...)
-- btree_gist 확장 없으면 ERROR
CREATE EXTENSION btree_gist; 선행.
(4) 복합 UNIQUE 순서 잘못
-- (a, b) UNIQUE 인덱스 박힌 상태
SELECT ... WHERE b = ... -- 인덱스 활용 X
자주 검색하는 컬럼이 앞.
(5) 운영에 CHECK 직접 추가
ALTER TABLE huge_table ADD CONSTRAINT chk_x CHECK (...);
-- 큰 테이블 = 전체 검증, 락 폭주
NOT VALID + VALIDATE 2단계 표준.
(1) NOT NULL·UNIQUE·PK·FK = 기본 4가지. (2) CHECK = 한 행 검증 (IMMUTABLE 함수). (3) 복잡 검증 = 트리거. (4) DEFERRABLE = 순환 참조·일괄 검증. (5) EXCLUDE = 범위·시간 충돌 방지. 앱 + DB 양쪽 검증.
한 줄 정리 — 제약 5종 + EXCLUDE. CHECK 깊이 = 테이블 수준·정규식·NOT VALID 2단계 추가. DEFERRABLE = 트랜잭션 끝 검증 (순환 참조). EXCLUDE = PG 특별, 범위·시간 충돌 방지. 운영 추가는 NOT VALID + VALIDATE 표준.
시험 직전 한 번 더 — 제약 깊이 입문자가 매번 헷갈리는 것
- 제약 6종 = NOT NULL·UNIQUE·PRIMARY KEY·FOREIGN KEY·CHECK·EXCLUDE
- CHECK = 한 행 검증 (단일 행 안 표현식)
- 컬럼 수준 vs 테이블 수준 (여러 컬럼)
- IMMUTABLE·STABLE 함수만 CHECK 가능
- VOLATILE (NOW·random 등) = CHECK X
- 정규식 CHECK = 이메일·전화번호 검증
- 복잡 검증 = 트리거 (다른 행·테이블 참조)
- DEFERRABLE = 트랜잭션 끝 검증
- INITIALLY DEFERRED = 기본 지연
- INITIALLY IMMEDIATE = 즉시 (SET 으로 지연 가능)
- SET CONSTRAINTS ALL DEFERRED
- 순환 참조 처리 = DEFERRABLE 필수
- EXCLUDE = PG 특별 (범위·시간 충돌)
- btree_gist 확장 필요
EXCLUDE USING GIST (col1 WITH op, col2 WITH op)- TSRANGE·INT4RANGE 활용
- 예약·일정·재고 시스템에 강력
- 복합 키 =
PRIMARY KEY (a, b) - 복합 UNIQUE = 컬럼 순서가 인덱스 활용 결정
- NOT VALID = 새 데이터만 검증
- VALIDATE CONSTRAINT = 백그라운드 검증
- 운영 추가 = NOT VALID + VALIDATE 2단계
- JPA Bean Validation = 앱 단 검증 (DB와 동시)
- @NotNull·@Email = 앱만, DB도 박아야 안전
- 제약 명명 =
chk_*·uniq_*·fk_* - 변경 = DROP + ADD (직접 ALTER X)
시리즈 다른 편
- Part 2 SQL Language 깊이: 19편 어휘 · 20편 SQL 문법 · 21편 DDL 개요 · 22편 CREATE TABLE 깊이 · 23편 (현재 글)
시리즈 다음 글
다음 글(24편)에서는 DML 개요 — INSERT·UPDATE·DELETE·MERGE의 큰 그림 + RETURNING·트랜잭션 통합.
공식 문서: PostgreSQL 18 — DDL: Constraints에서 더 자세한 사양을 확인할 수 있어요.