한 줄 정의

외래키는 부모 테이블의 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의 기본값을 그대로 쓰지 말고, 프로젝트의 성격에 맞게 명시적으로 결정해야 합니다.

관련 개념

출처

  • Real MySQL 8.0 (1권), 8.10 외래키