백엔드 데이터 인프라 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 로 추가합니다.

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(자바 ORM 표준 스펙) 에서는 @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(JPA 구현체) 가 ddl-auto=update 모드에서 FK 제약을 자동으로 만들어요. 단, ON DELETE 옵션은 @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 로 쓰는 방법도 있습니다. 운영에서 영구 비활성화는 금물 — 무결성이 깨집니다.

외래 키 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 제약과 충돌하지 않습니다. 다만 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 옵션 차이

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!