백엔드 데이터 인프라 23편 — 제약 깊이 CHECK DEFERRABLE EXCLUDE

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 23편. 제약 깊이 — 복합 CHECK 표현식·DEFERRABLE·EXCLUDE·복합 키와 운영 함정 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 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단계 표준.

🎯 제약 5단계

(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)

시리즈 다른 편

시리즈 다음 글

다음 글(24편)에서는 DML 개요 — INSERT·UPDATE·DELETE·MERGE의 큰 그림 + RETURNING·트랜잭션 통합.

공식 문서: PostgreSQL 18 — DDL: Constraints에서 더 자세한 사양을 확인할 수 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!