학습 목표
- FK 의 기본 동작을 넘어, 실무에서 실제로 부딪히는 이슈를 원인·재현·해결까지 한 번에 정리한다
errno 150/1452/1451/3730/3780/1822— FK 에러 코드를 보고 바로 원인을 짚는다- "우리 회사는 FK 를 안 쓴다"는 말의 진짜 트레이드오프를 이해한다 (애플리케이션 레벨 FK)
- ORM(JPA/Hibernate)·마이그레이션·온라인 스키마 변경·파티셔닝·샤딩에서 FK 가 일으키는 실전 함정을 피한다
- FK 로 생기는 데드락·락 병목·고아 행을 진단하고 정리하는 쿼리를 손에 익힌다
선행 스텝: Step 13 — 제약 조건과 정규화 (특히 13-5 ~ 13-6) 예상 소요: 70분
이 부록은 Step 13 의 FK 절을 다 읽었다고 가정합니다. 기본 문법(5가지
ON DELETE/UPDATE옵션, 인덱스 자동 생성)은 반복하지 않고, 그 다음에 실무에서 터지는 것들만 모았습니다.
이 부록의 예제는 s13b_ 접두사 테이블만 씁니다. 공용 테이블(customers, orders …)은 건드리지 않습니다.
💡 이 부록의 SQL 은
practice.sql에 그대로 담겨 있습니다. 에러 메시지는 직접 재현해 보는 것이 가장 잘 외워집니다.
FK 는 선언 한 줄이지만, InnoDB 는 매 DML 마다 아래를 자동으로 합니다.
| 작업 | FK 가 추가로 하는 일 |
|---|---|
| 자식 INSERT / UPDATE | 부모에 그 값이 있는지 조회 → 없으면 거부(1452), 부모 행에 공유락(S) |
| 부모 DELETE | 자식이 참조하는지 조회 → 옵션대로 처리(RESTRICT면 거부 1451, CASCADE면 자식 삭제) |
| 부모 PK UPDATE | ON UPDATE 옵션대로 자식 전파 |
| 테이블 DROP | 참조하는 자식이 있으면 거부(3730) |
핵심 3줄
로그에서 이 번호만 보고 원인을 짚을 수 있어야 합니다.
| 에러 | 언제 | 원인 | 해결 |
|---|---|---|---|
1452 Cannot add or update a child row | 자식 INSERT/UPDATE | 부모에 없는 값을 참조 | 부모 먼저 넣기 / 값 확인 / NULL 허용이면 NULL |
1451 Cannot delete or update a parent row | 부모 DELETE/UPDATE | 자식이 아직 참조 중 (RESTRICT) | 자식 먼저 정리 / CASCADE 검토 |
3730 Cannot drop table ... referenced by a foreign key | 부모 DROP TABLE | 다른 테이블이 참조 중 | 자식 FK 먼저 drop / SET FOREIGN_KEY_CHECKS=0 |
3780 columns ... are incompatible | FK 생성 시 | 부모·자식 컬럼 타입/부호/콜레이션 불일치 | 타입 정확히 일치시키기 (아래 B-3) |
1822 Missing index for constraint | FK 생성 시 | 부모 참조 컬럼에 인덱스 없음 | 부모 컬럼에 PK/UNIQUE/INDEX |
150 (errno: 150) | FK 생성 시 (일반) | 위 원인들의 InnoDB 내부 코드 | SHOW ENGINE INNODB STATUS 의 LATEST FOREIGN KEY ERROR 확인 |
💡 FK 생성이 실패하면 항상 이 명령부터:
출력의
LATEST FOREIGN KEY ERROR섹션에 어떤 컬럼이 왜 안 맞는지 사람이 읽을 수 있게 적혀 있습니다.errno 150만 보고 헤매지 마세요.
FK 는 부모·자식 컬럼의 타입이 완벽히 같아야 걸립니다. "둘 다 INT처럼 보이는데 왜 안 되지?"의 90%는 아래입니다.
| 불일치 요소 | 예 | 결과 |
|---|---|---|
| 부호(SIGNED/UNSIGNED) | 부모 INT UNSIGNED, 자식 INT | ❌ 3780 |
| 길이/정밀도 | 부모 BIGINT, 자식 INT | ❌ 3780 |
| 문자셋/콜레이션 | 부모 utf8mb4_0900_ai_ci, 자식 utf8mb4_general_ci | ❌ 3780 |
| 부모에 인덱스 없음 | 참조 컬럼이 PK/UNIQUE/INDEX 아님 | ❌ 1822 |
⚠️ 함정 — 문자열 FK 의 콜레이션 지옥
VARCHAR컬럼을 FK 로 걸 때, 부모와 자식의 콜레이션이 1글자라도 다르면 FK 가 안 걸립니다. 게다가 콜레이션이 다르면 이후 조인할 때도 인덱스를 못 타는(암묵 변환) 별개의 성능 문제까지 생깁니다. 그래서 FK 키는 되도록 정수 대리키(surrogate key) 를 쓰는 것이 실무 정석입니다.
💡 실무 팁: 테이블을 여러 번에 나눠 만들 때는
CREATE TABLE ... LIKE로 부모 컬럼 정의를 복사하거나, 팀 컨벤션으로 "ID 는 무조건BIGINT UNSIGNED" 처럼 못 박아 두면 이 부류의 사고가 통째로 사라집니다.
FK 는 부모가 먼저 존재해야 한다는 순서 제약을 만듭니다. 실무에서 이게 마이그레이션 스크립트와 덤프 복원을 괴롭힙니다.
| 작업 | 올바른 순서 |
|---|---|
| 테이블 생성 | 부모 → 자식 |
| 데이터 적재 | 부모 → 자식 |
| 데이터 삭제 | 자식 → 부모 |
| 테이블 DROP | 자식 → 부모 |
A → B, B → A 처럼 서로 참조하면 "누구를 먼저?"가 성립하지 않습니다. 해법:
⚠️ 함정 —
mysqldump복원이 FK 순서로 실패 덤프 파일은 테이블을 알파벳순으로 복원하는데, 자식이 부모보다 먼저 나오면1452로 터집니다. 그래서mysqldump는 기본적으로 덤프 앞머리에SET FOREIGN_KEY_CHECKS=0을 넣습니다. 직접 만든 마이그레이션 스크립트에는 이게 없으니 순서를 맞추거나 검사를 꺼야 합니다.
💡 실무 팁 — 전체 스키마를 안전하게 drop
대규모 서비스(특히 대형 인터넷 기업)에서 DB FK 를 아예 선언하지 않는 곳이 많습니다. 신입이 "정합성 어떻게 지켜요?"라고 물으면 팀장은 "애플리케이션에서 지켜요"라고 답합니다. 이게 무슨 뜻이고 트레이드오프가 뭔지 알아야 합니다.
| DB 레벨 FK | 애플리케이션 레벨 FK | |
|---|---|---|
| 정합성 보증 | DB 가 100% 강제 | 코드에 의존 (버그·경합에 뚫림) |
| 쓰기 성능 | 부모 조회·락 오버헤드 | 없음 (더 빠름) |
| 샤딩 | 부모/자식이 다른 샤드면 불가능 | 가능 (앱이 조율) |
| 온라인 스키마 변경 | gh-ost/pt-osc 가 싫어함 (B-6) | 자유로움 |
| 대량 삭제 | CASCADE 폭발 위험 | 앱이 배치로 제어 |
| 실수 방어 | 잘못된 참조를 DB 가 즉시 차단 | 차단 못 함 → 고아 데이터 축적 |
결론(실무 기준선)
⚠️ 함정 — FK 없이 컬럼명으로 관계를 추측 FK 를 선언하지 않으면
information_schema에 관계 정보가 없어서, 새 개발자는order_id,customer_id같은 컬럼 이름으로 관계를 추측해야 합니다. ERD 툴도 관계선을 못 그립니다. FK 는 정합성 도구이자 살아있는 문서이기도 합니다.
CascadeType.REMOVE / orphanRemoval 와 DB ON DELETE CASCADE 를 동시에 걸지 마세요. 둘 다 켜면 이중 삭제 로직이 되어, 앱이 자식을 하나씩 지우는 동안 DB 도 CASCADE 를 돌립니다. 보통은 DB CASCADE 를 쓰고 앱 cascade 는 끄거나, 반대로 통일하세요.@JoinColumn 으로 FK 를 만들면 InnoDB 가 자식 인덱스를 자동 생성하지만, 일부 스키마 생성 전략/DB 에선 안 만들어질 수 있습니다. 자식 FK 컬럼에 인덱스가 없으면 부모 삭제 시 자식 풀스캔이 일어나 대형 테이블에서 재앙이 됩니다.@ManyToOne 이 NOT NULL FK 인데 앱이 부모 없이 자식을 저장하려 하면 1452.ddl-auto=update 가 만든 FK 는 이름이 FKxxxx 로 랜덤이라 나중에 추적이 안 됩니다.대형 테이블을 무중단으로 바꾸는 이 도구들은 FK 를 극도로 싫어합니다. 원리상 "새 테이블을 만들어 복사 후 원자적 교체(RENAME)"인데, 원본을 참조하는 자식 FK 가 교체 순간 깨지기 때문입니다.
pt-osc 는 --alter-foreign-keys-method 옵션으로 우회하지만 위험하고 느립니다.gh-ost 는 아예 FK 걸린 테이블을 기본적으로 거부합니다.💡 실무 팁: 대형 테이블 + 무중단 변경이 잦은 환경이라면, 이것이 애플리케이션 레벨 FK 를 선택하는 강력한 현실적 이유가 됩니다. 반대로 FK 를 유지하려면 스키마 변경을 서비스 저부하 시간대에 계획해야 합니다.
⚠️ 함정: 파티셔닝된 테이블에는 FK 를 걸 수 없습니다 (파티션 테이블이 FK 의 부모도, 자식도 될 수 없음). 시계열 데이터를 파티셔닝하려면 FK 를 포기하거나, 정합성을 앱에서 지켜야 합니다. Step 21 과 함께 보세요.
자식 INSERT 는 부모 행에 공유락(S) 을 겁니다("내가 참조하는 동안 이 부모 지우지 마"). "게스트 사용자", "기본 카테고리" 같은 인기 부모 행은 수많은 자식 INSERT 가 동시에 S락을 걸고, 그 부모를 누군가 UPDATE(X락) 하려는 순간 전부 대기합니다.
💡 자주 참조되는 마스터 데이터(코드값)는 거의 안 바뀌게 설계하고, 바꿀 일이 있으면 저부하 시간에 하세요.
두 트랜잭션이 자식을 넣으며 서로 다른 순서로 부모 S락을 잡고, 이어서 부모를 갱신하려다 교착됩니다.
InnoDB 가 한쪽을 자동 롤백합니다:
⚠️ 함정 — 데드락은 "정상"이다. 없앨 수 없습니다. 애플리케이션은
1213을 만나면 트랜잭션을 재시도하는 로직을 반드시 갖춰야 합니다. 💡 진단:SHOW ENGINE INNODB STATUS\G→LATEST DETECTED DEADLOCK섹션에 두 트랜잭션이 잡은/기다린 락이 전부 나옵니다. 예방: 모든 트랜잭션이 부모를 같은 순서(예: id 오름차순)로 접근하게 만드세요.
SET FOREIGN_KEY_CHECKS=0 으로 넣었거나, FK 없이 운영했거나, 과거에 잘못 지운 데이터 때문에 부모 없는 자식이 생깁니다. FK 를 뒤늦게 추가하려면 먼저 이걸 청소해야 합니다(고아 행이 있으면 ALTER TABLE ... ADD FOREIGN KEY 가 1452 로 실패).
⚠️ 함정:
c.pid IS NOT NULL을 빼먹으면, FK 컬럼이 NULL(정상적으로 "참조 없음")인 행까지 고아로 잡습니다. NULL 은 고아가 아닙니다.
💡 실무 팁 — 대형 테이블에 뒤늦게 FK 붙이기:
ALTER TABLE ... ADD FOREIGN KEY는 전체 자식 행을 검증하므로 대형 테이블에서 오래 걸리고 락을 잡습니다. 저부하 시간대에, 가능하면 온라인 DDL 지원 여부를 확인하고 진행하세요. FK 없는 프로젝트에 관계를 문서화만 하고 싶다면, 실제 FK 대신 정기 고아 점검 배치로 갈음하기도 합니다.
DELETE FROM t | TRUNCATE TABLE t | |
|---|---|---|
| 자식이 참조 중일 때 | RESTRICT 면 거부, CASCADE 면 전파 | 자식 존재 시 무조건 거부 (1701) |
| AUTO_INCREMENT | 유지 | 리셋 |
| 속도 | 행 단위 (느림) | 테이블 재생성 (빠름) |
⚠️ 함정:
TRUNCATE는ON DELETE CASCADE를 타지 않습니다. CASCADE 가 걸려 있어도 자식이 하나라도 있으면 그냥 거부합니다. 전체를 비우려면 자식부터TRUNCATE하거나SET FOREIGN_KEY_CHECKS=0.
Step 13 에서 봤듯 자식 FK 컬럼의 인덱스는 InnoDB 가 자동 생성합니다. 하지만 여기에 두 가지 실무 이슈가 있습니다.
(pid, created_at) 복합으로 조회한다면, 이미 그 복합 인덱스가 앞에 pid 를 포함하므로 FK 자동 인덱스는 중복입니다. → 복합 인덱스를 먼저 만들면 InnoDB 는 그걸 FK 인덱스로 재사용하고 별도 인덱스를 안 만듭니다.DROP INDEX 가 1553 으로 거부됩니다. 인덱스를 바꾸려면 FK 를 먼저 떼거나, 대체 인덱스를 먼저 만들어야 합니다.💡 실무 팁: FK 를 걸기 전에 실제 조회 패턴에 맞는 복합 인덱스를 먼저 설계하세요. 그러면 (1) FK 자동 인덱스 중복을 피하고 (2) 부모 삭제/자식 조회 성능도 같이 챙깁니다.
설계 시
BIGINT UNSIGNED)로 통일 — 타입/콜레이션 불일치(3780) 원천 차단ON DELETE 를 의식적으로 선택: 자식 적으면 CASCADE, 많으면 RESTRICT + 앱 배치 삭제ON DELETE SET NULL 을 쓸 거면 자식 FK 컬럼을 NULL 허용으로ON DELETE SET DEFAULT 는 InnoDB 가 무시하니 쓰지 말 것운영 시
SET FOREIGN_KEY_CHECKS=0 → 적재 → 켠 뒤 고아 행 검증 필수1213(데드락) 을 만나면 재시도gh-ost) 계획 시 FK 존재 여부 확인에러 만나면
1452 → 부모에 값이 있는가 / 적재 순서 / NULL 허용 여부1451 → 자식이 참조 중 / RESTRICT / CASCADE 검토3730 → DROP 순서 / FOREIGN_KEY_CHECKS=03780 / 1822 / errno 150 → SHOW ENGINE INNODB STATUS\G 의 LATEST FOREIGN KEY ERROR되짚기: Step 13 — 제약 조건과 정규화 · 락과 트랜잭션의 원리는 Step 19 · NULL 규칙은 부록 A.