백엔드 데이터 인프라 15편. 외래 키로 참조 무결성을 강제하는 표준 패턴과 ON DELETE·ON UPDATE 옵션, 운영 함정까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 15편이에요. 7편 관계형 모델 에서 "외래 키 = 행 사이 관계" 라고 짧게 소개했죠. 이번 15편은 그 외래 키를 운영 표준으로 깊이 풀어 가요.
외래 키가 보장하는 것 — 참조 무결성
users orders
+----+-------+ +----+---------+--------+
| id | name | ← | id | user_id | amount |
+----+-------+ +----+---------+--------+
| 1 | Alice | | 1 | 1 | 10000 |
+----+-------+ | 2 | 1 | 5000 |
| 3 | 999 | 3000 | ← 999는 존재 안 함!
+----+---------+--------+
참조 무결성 위반 — orders #3의 user_id 999가 users에 없음. 외래 키 없으면 — 이런 "고아 데이터" 가 박힐 수 있어요. 외래 키 = DB가 직접 거부.
INSERT INTO orders (user_id, amount) VALUES (999, 3000);
-- ERROR: insert or update on table "orders" violates foreign key constraint
외래 키 선언
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
amount INTEGER NOT NULL
);
REFERENCES 테이블(컬럼) = 외래 키 선언. 컬럼 정의 안 인라인 형태. 또는 별도 제약:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount INTEGER NOT NULL,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
);
기존 테이블에 추가:
ALTER TABLE orders
ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
다중 컬럼 외래 키
CREATE TABLE order_items (
order_id BIGINT,
line_no INTEGER,
product_id BIGINT,
quantity INTEGER,
PRIMARY KEY (order_id, line_no),
FOREIGN KEY (order_id) REFERENCES orders(id)
);
복합 키 + 복합 외래 키. JPA에서는 @EmbeddedId 매핑.
ON DELETE — 부모 삭제 시 자식
14편 DELETE 에서 다룬 5가지 옵션 — 자세히.
NO ACTION (기본)
user_id BIGINT REFERENCES users(id) ON DELETE NO ACTION
자식이 있으면 부모 삭제 거부. 가장 안전. 운영 표준.
RESTRICT
ON DELETE RESTRICT
NO ACTION 과 거의 같지만 — "즉시 검증". NO ACTION은 트랜잭션 끝에 검증 (deferrable 옵션 있을 때 차이).
CASCADE
ON DELETE CASCADE
부모 삭제 시 자식도 함께 삭제. 강력하지만 위험. 사용자 한 번 삭제로 주문 1만 건 사라질 수 있음.
적합한 시나리오: - 부모와 자식이 "한 묶음 객체" — 예: order ↔ order_items - 부모 없으면 자식 의미 없음
부적합: - 비즈니스 데이터 — 운영·감사 차원에서 위험
SET NULL
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL
부모 삭제 시 — 자식 컬럼을 NULL. "부모 없어졌지만 자식은 남기고 싶을 때" — 사용자 탈퇴 후 주문 기록 보존.
단 — 외래 키 컬럼이 NOT NULL 이면 SET NULL 불가.
SET DEFAULT
user_id BIGINT NOT NULL DEFAULT 0 REFERENCES users(id) ON DELETE SET DEFAULT
기본값으로. 거의 안 씀.
ON UPDATE — 부모 키 변경 시
같은 옵션 — 단 "부모의 키 변경" 은 실무에 거의 없음 (인공 키 사용 시). 자연 키 사용 시 의미 있음.
ON UPDATE CASCADE -- 부모 ID 변경 → 자식도 함께
DEFERRABLE — 트랜잭션 끝에 검증
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id)
DEFERRABLE INITIALLY DEFERRED
기본은 "매 SQL마다 즉시 검증". DEFERRABLE 박으면 — 트랜잭션 끝(COMMIT 직전) 까지 검증 미룸. 순환 참조·복잡한 데이터 이관 시 유용.
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
-- 순환 의존 INSERT (일시 무결성 위반 OK)
INSERT INTO ...;
INSERT INTO ...;
COMMIT; -- 여기서 한 번에 검증
외래 키와 인덱스
중요 — PG는 외래 키 컬럼에 자동 인덱스 안 만듦 (부모의 PK는 자동, 자식 FK는 수동).
-- 부모 PK = 자동 인덱스 (users.id)
-- 자식 FK = 수동 인덱스 필요
CREATE INDEX idx_orders_user_id ON orders(user_id);
없으면 — 부모 삭제 시 "자식 어디 있나" 풀스캔. 큰 테이블에서 락 폭증·시간 폭주.
룰 — 외래 키 컬럼에 무조건 인덱스. JOIN·삭제 성능 모두 영향.
Spring JPA 의 외래 키
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
@JoinColumn(name = "user_id") = REFERENCES users(id) 매핑. Hibernate가 ddl-auto=update 모드에 자동 FK 제약 생성. 단 — @OnDelete로 옵션 명시:
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id")
private User user;
외래 키 비활성화 — 임시
데이터 이관·테스트 시 — 일시 비활성화.
ALTER TABLE orders DROP CONSTRAINT fk_user;
-- ... 작업 ...
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
또는 트랜잭션 안 DEFERRED. 운영에서 영구 비활성화는 X — 무결성 깨짐.
외래 키 vs "앱 코드로 무결성"
논쟁: - FK 박기 = DB가 강제, 안전, 단 성능 일부 - FK 안 박기 + 앱 코드 검증 = 유연, 단 "코드 버그 = 데이터 깨짐"
한국 회사 일반 = FK 무조건 박기. 비용 작은 안전망. "FK 빼서 성능 얻는 시나리오" 는 극단적 대용량(수십억 행) 또는 분산 시스템.
함정 5가지
(1) FK 컬럼 인덱스 누락
가장 흔한 함정. 무조건 인덱스.
(2) CASCADE 남용
운영 데이터에 CASCADE 박으면 — 작은 실수가 큰 사고. NO ACTION + 코드 처리.
(3) FK 빼고 시작
"빨리 개발" 한다고 FK 빼면 — 운영 단계에서 "고아 데이터" 가 박혀 무결성 깨짐. 복구 매우 어려움.
(4) Soft Delete + FK
DELETE FROM users WHERE id = 1;
-- 실제로는 deleted_at UPDATE
-- → orders.user_id 는 그대로 = OK
Soft Delete는 FK 제약과 충돌 X. 다만 — deleted_at IS NULL 필터 일관성 필요.
(5) 운영 ALTER ADD CONSTRAINT
큰 테이블에 사후 FK 추가 = 풀스캔. NOT VALID 로 단계적:
-- 1. 새 데이터에만 검증
ALTER TABLE orders ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- 2. 기존 데이터 검증 (별도 작업)
ALTER TABLE orders VALIDATE CONSTRAINT fk_user;
NOT VALID = 새 데이터에만 즉시 적용, 기존은 백그라운드 검증. 운영 다운타임 0.
(1) 무조건 박기. (2) FK 컬럼 인덱스 필수. (3) 운영 표준 = NO ACTION. (4) CASCADE는 의존 객체만 (order_items 같은). 한국 회사 99% 적용.
한 줄 정리 — 외래 키 = 참조 무결성 강제. REFERENCES + ON DELETE 옵션 5가지 (NO ACTION·CASCADE·SET NULL·SET DEFAULT·RESTRICT). FK 컬럼은 무조건 인덱스. CASCADE 신중. 운영 추가는 NOT VALID + VALIDATE 2단계.
시험 직전 한 번 더 — 외래 키 입문자가 매번 헷갈리는 것
- REFERENCES users(id) = FK 선언
- 인라인 vs CONSTRAINT 별도
- 다중 컬럼 =
FOREIGN KEY (a, b) REFERENCES t(a, b) - ON DELETE 옵션 5개 = NO ACTION·RESTRICT·CASCADE·SET NULL·SET DEFAULT
- 운영 표준 = NO ACTION
- CASCADE 신중 = 의존 객체만 (order_items)
- SET NULL = 부모 없어져도 자식 보존
- ON UPDATE = 인공 키 사용 시 의미 X
- DEFERRABLE INITIALLY DEFERRED = 트랜잭션 끝 검증
- 순환 참조 데이터 이관에 활용
- FK 컬럼 인덱스 자동 X — 수동 박기 필수
- 인덱스 없으면 = 부모 삭제 시 풀스캔
- @ManyToOne + @JoinColumn = JPA FK 매핑
- @OnDelete(action) = Hibernate FK 옵션
- 외래 키 일시 비활성화 = DROP CONSTRAINT
- NOT VALID + VALIDATE = 운영 ALTER 2단계
- FK 빼고 시작 = 위험 (고아 데이터)
- Soft Delete + FK = 충돌 X
- 한국 회사 99% = FK 박기
- 분산 시스템 = FK 빼는 경우 있음
- 1차 캐시·N+1 = FK 인덱스 영향 큼
- 면접 단골 = ON DELETE 옵션 차이
시리즈 다른 편
- Part 1 PostgreSQL 튜토리얼: 1편 · 9편 CREATE TABLE · 10편 INSERT · 11편 SELECT · 12편 JOIN · 13편 UPDATE · 14편 DELETE · 15편 (현재 글)
시리즈 다음 글
다음 글(16편)에서는 뷰(VIEW) — 쿼리를 테이블처럼 — VIEW·MATERIALIZED VIEW 표준 패턴.
공식 문서: PostgreSQL 18 — Tutorial: Foreign Keys에서 더 자세한 사양을 확인할 수 있어요.