한 줄 정의
외래키는 부모 테이블의 PK를 자식 테이블이 참조 하여 관계 무결성을 보장하는 제약 조건이며, InnoDB에서만 스토리지 엔진 레벨로 지원하고 부모/자식 양쪽 모두 인덱스가 필요 합니다.
쉽게 말하면
외래키는 “이 칼럼의 값은 다른 테이블에 반드시 존재해야 한다” 는 규칙입니다.
orders.user_id에 외래키를 걸면,users에 없는 사용자 ID로 주문을 생성할 수 없습니다users의 사용자를 삭제하면, 관련된orders도 자동 처리(CASCADE)되거나 삭제를 막을 수 있습니다
단순히 편리한 기능처럼 보이지만, 잠금이 부모-자식 사이로 전파 되기 때문에 실무에서는 데드락의 주범으로 꼽히기도 합니다.
왜 중요한가?
외래키는 장점과 단점이 명확히 갈리는 기능 입니다.
- 데이터 정합성을 DB가 강제한다는 점에서는 훌륭합니다
- 하지만 대용량 서비스에서는 성능/운영 부담 때문에 일부러 사용하지 않는 경우가 많습니다
이 트레이드오프를 이해해야 “외래키를 걸어야 하는가?”를 팀 내에서 제대로 논의할 수 있습니다.
핵심 내용
외래키의 기본 동작
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount INT,
INDEX ix_user_id (user_id),
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;참조 무결성 동작 옵션
부모 테이블의 레코드가 변경/삭제 될 때 자식 테이블이 어떻게 반응할지 결정합니다.
| 옵션 | 의미 |
|---|---|
CASCADE | 부모 변경 시 자식도 함께 변경/삭제 |
SET NULL | 자식의 FK 칼럼을 NULL로 설정 |
NO ACTION / RESTRICT | 자식이 참조 중이면 부모 변경/삭제 차단 |
SET DEFAULT | 기본값으로 (InnoDB는 파서만 허용, 실제 동작 안 함) |
인덱스 요구 사항
외래키는 부모와 자식 양쪽 모두 인덱스 가 필요합니다.
- 부모 테이블 : 참조되는 칼럼에 PK 또는 유니크 인덱스 필요 (보통 PK)
- 자식 테이블 : FK 칼럼에 인덱스 필요 (InnoDB는 없으면 자동 생성)
자식 쪽 인덱스가 없으면 부모의 UPDATE/DELETE 시마다 자식 테이블 풀 스캔 이 일어나므로 필수입니다.
외래키와 잠금 전파
외래키의 진짜 무서움은 잠금의 전파 입니다.
sequenceDiagram participant TX_A as 트랜잭션 A participant Parent as users (부모) participant Child as orders (자식) participant TX_B as 트랜잭션 B TX_A->>Parent: UPDATE users SET ... WHERE id=1 Parent->>Child: FK 확인을 위해 orders의 user_id=1 검사 Note over Child: orders의 관련 레코드에 <br/>읽기 잠금(S-lock) 부여 TX_B->>Child: UPDATE orders WHERE user_id=1 Note over TX_B: A가 건 S-lock 때문에 대기
- 부모 변경 시, InnoDB는 자식 테이블의 관련 레코드에 공유 잠금(S-lock) 을 겁니다
- 자식 변경 시, 부모 레코드에도 공유 잠금 을 겁니다
- 여러 트랜잭션이 얽히면 데드락이 쉽게 발생 합니다
foreign_key_checks 설정
대량 데이터 로드나 특정 작업 시 외래키 체크를 일시 비활성화할 수 있습니다.
SET foreign_key_checks = OFF;
-- 대량 INSERT/UPDATE
SET foreign_key_checks = ON;CASCADE동작까지 건너뜁니다 (주의)- 세션 단위 설정이므로 다른 커넥션에는 영향 없음
- 운영 중 무분별한 사용은 데이터 불일치 를 만들 수 있음
외래키를 쓰지 않는 실무 선택
대규모 서비스에서 외래키를 의도적으로 제거하는 경우가 많습니다. 그 이유들:
1. 샤딩/파티셔닝과의 충돌
테이블이 여러 물리 DB에 분산되면 외래키가 아예 동작할 수 없습니다. 샤딩을 염두에 둔다면 처음부터 외래키 없이 설계합니다.
2. 스키마 변경의 부담
외래키가 있으면 DDL 작업이 복잡해집니다.
- 부모 테이블의 칼럼 타입 변경 시 자식도 함께 변경해야 함
pt-online-schema-change같은 도구 사용 시 추가 옵션 필요- 파티셔닝 적용 시 외래키 제거가 선행되어야 함
3. 애플리케이션 레벨 체크
외래키 제약을 애플리케이션 코드 에서 처리하는 선택입니다.
- 장점 : 유연함, 성능, 확장성
- 단점 : 버그 시 데이터 정합성 깨짐
4. 성능
외래키 체크는 매 변경마다 추가 조회/잠금 을 발생시킵니다. 대량 트랜잭션에서는 이 오버헤드가 누적됩니다.
외래키가 유용한 경우
- 작은/중간 규모 서비스 에서 데이터 정합성을 DB에 맡기고 싶을 때
- 레거시 마이그레이션 에서 점진적 정합성 확보가 필요할 때
- 관리자/백오피스 시스템 처럼 성능보다 정확성이 중요한 경우
정리
외래키 사용 결정 체크리스트
| 체크 항목 | 외래키 권장 | 외래키 비권장 |
|---|---|---|
| 데이터 규모 | 소/중 | 대 |
| 샤딩 계획 | 없음 | 있음 |
| 쓰기 트래픽 | 낮음 | 높음 |
| 정합성 중요도 | 매우 중요 | 애플리케이션으로 커버 가능 |
| 팀의 DB 운영 역량 | 제한적 | 고도화됨 |
외래키 vs 애플리케이션 체크
| 기준 | DB 외래키 | 애플리케이션 체크 |
|---|---|---|
| 정합성 | 강제됨 | 코드 책임 |
| 성능 | 오버헤드 있음 | 상대적 빠름 |
| 유연성 | 낮음 | 높음 |
| 데드락 위험 | 있음 | 없음 (FK 전파 관점) |
| 샤딩 대응 | 불가 | 가능 |
내 생각
-
외래키는 “당연히 거는 것”이 아닙니다. 회사 규모와 아키텍처 방향에 따라 전략적으로 선택해야 합니다. 스타트업 초기에는 외래키가 유리할 수 있지만, 트래픽이 증가하고 샤딩을 고려하는 시점이 오면 제거가 고민 됩니다.
-
외래키 제거 시에는 애플리케이션에 체크 로직을 먼저 이식 한 뒤 제거해야 합니다. 순서를 거꾸로 하면 잠시라도 정합성 깨진 데이터가 유입될 수 있습니다.
-
CASCADE는 편리하지만 위험 합니다. 특히
ON DELETE CASCADE는 의도치 않은 대량 삭제로 이어질 수 있습니다. 정말 필요한 경우가 아니라면NO ACTION이나 애플리케이션 레벨 정리 를 권장합니다. -
Rails, Django 같은 프레임워크의 ORM은 외래키를 기본으로 깔아둡니다. ORM의 기본값을 그대로 쓰지 말고, 프로젝트의 성격에 맞게 명시적으로 결정해야 합니다.
관련 개념
- Ch04-2 InnoDB 스토리지 엔진 — 외래키 지원,
foreign_key_checks - Ch08-8 클러스터링 인덱스 — PK 설계가 외래키에 미치는 영향
- Ch05-3 InnoDB 스토리지 엔진 잠금 — 잠금 전파
출처
- Real MySQL 8.0 (1권), 8.10 외래키