백엔드 데이터 인프라 15편 — 외래 키 참조 무결성

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

백엔드 데이터 인프라 15편. 외래 키로 참조 무결성을 강제하는 표준 패턴과 ON DELETE·ON UPDATE 옵션, 운영 함정까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 15편 — 외래 키 참조 무결성

이 글은 백엔드 데이터 인프라 시리즈 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 의 외래 키

자바 백엔드 입문 46편 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.

🎯 외래 키 4룰

(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 옵션 차이

시리즈 다른 편

시리즈 다음 글

다음 글(16편)에서는 뷰(VIEW) — 쿼리를 테이블처럼 — VIEW·MATERIALIZED VIEW 표준 패턴.

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

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

답글 남기기

error: Content is protected !!