부모 없는 자식은 못 들어온다,
외래키 (FK) 와 무결성
4-3까지는 관계가 어떻게 생겼는지를 봤습니다. 이번엔 그 관계를 DB가 직접 강제하는 장치 — 외래키(FK)입니다. 존재하지 않는 회원의 주문, 주문이 남아있는 회원의 삭제 — 전부 DB 선에서 거부됩니다. shop DB에서 직접 위반을 시도하며 확인해봅니다.
이번 섹션의 여정
완료 ✓
지금 여기
설계도 독해
정규화 · 실무 ERD · 종합 실습 ⭐
다음 마일스톤
관계를 그리는 법에서, 지키는 법으로
4-1에서 관계로 생각하는 법을, 4-2에서 부모-자식 구조를, 4-3에서 1:1 · 1:N · N:M 세 가지 유형을 배웠습니다. 그런데 질문이 하나 남아요 — 우리가 머리로 아는 이 관계를, DB는 어떻게 지킬까요? 누군가 실수로 ‘존재하지 않는 회원의 주문’을 밀어 넣으면 어떻게 될까요?
그걸 막아주는 장치가 바로 외래키(Foreign Key, FK) 입니다.
FK = 부모-자식 관계를 DB가 지켜주는 안전망.
부모 없는 자식이 못 들어오게, 자식 딸린 부모가 함부로 사라지지 않게 — DB가 직접 막아줍니다.
FK 의 역할 — 한 줄의 계약
shop의 orders 테이블을 다시 볼게요. 사실 2-3에서 Alt + X 로 실행한 init.sql 안에, 이 한 줄이 이미 들어 있었습니다.
CREATE TABLE orders (
order_id INT NOT NULL AUTO_INCREMENT COMMENT '주문ID',
member_id INT NOT NULL COMMENT '회원ID(FK)',
ordered_at DATE NOT NULL COMMENT '주문일',
PRIMARY KEY (order_id),
CONSTRAINT fk_orders_member
FOREIGN KEY (member_id) REFERENCES members(member_id)
);마지막 두 줄이 핵심입니다. “orders 의 member_id 는 반드시 members 의 member_id 에 존재하는 값이어야 한다” — 이 계약을 DB에 등록하는 문장이에요. 계약이 등록된 순간부터 DB는 이런 시도를 전부 거부합니다.
| 시도 | FK 의 반응 |
|---|---|
| 존재하지 않는 회원ID(9999)로 주문 INSERT | ❌ 거부 — 부모 없는 자식 차단 |
| 주문이 남아있는 회원 DELETE | ❌ 거부 — 자식 딸린 부모 보호 |
| members 에 없는 ID 로 member_id UPDATE | ❌ 거부 — INSERT 와 같은 원리 |
이렇게 데이터가 항상 일관된 상태를 유지하는 것 — 이걸 무결성(integrity) 이라고 부릅니다.
그럼 FK가 없으면 무슨 일이 벌어질까요? 두 DB를 비교해보면 답이 보입니다.
유령 데이터가 조용히 쌓입니다
회원 1번을 삭제해도 그 회원의 주문 50건은 그대로 남습니다. 존재하지 않는 회원ID를 가리키는 유령 데이터 — 이런 게 100건, 1,000건 쌓이면 “회원ID 1의 매출 1,000만원” 인데 회원 정보는 없는, 분석이 어긋나는 DB가 됩니다. 누구야 대체?
사고가 나기 전에 거부합니다
같은 삭제를 시도하면 DB가 에러로 즉시 거부합니다. 유령 데이터가 태어날 기회 자체가 없어요. CREATE TABLE 의 FK 한 줄이 이 모든 사고를 원천 차단합니다.
직접 실험 — DB 가 진짜 막아줄까
백문이 불여일타 — DBeaver SQL Editor에서 직접 FK를 화나게 해봅시다. 두 가지 위반을 시도합니다.
USE shop;
-- 실험 ① 존재하지 않는 회원(9999번)으로 주문 만들기
INSERT INTO orders (member_id, ordered_at) VALUES (9999, '2026-05-11');
-- 실험 ② 주문이 남아있는 회원(김철수, 1번) 삭제하기
DELETE FROM members
WHERE
member_id = 1;실험 ① — 회원 9999번은 members 에 없습니다. 부모 없는 자식을 넣으려는 시도죠.
실험 ② — 이번엔 반대 방향입니다. 김철수(1번)는 주문을 6건이나 갖고 있는 부모예요.
두 시도 모두 거부됐으니 DB는 아무것도 바뀌지 않았습니다. 설령 실험하다
뭔가 꼬여도 — init.sql 을 다시 Alt + X 하면 언제든 처음 상태로 돌아옵니다.
ON DELETE 옵션 — 부모가 사라질 때 자식은
실험 ②에서 삭제가 거부된 건 FK의 기본 정책 때문입니다. 사실 FK를 만들 때 “부모가 삭제되면 자식을 어떻게 할지” 를 직접 정할 수 있어요. 선택지는 세 가지입니다.
| 옵션 | 부모 삭제 시 | 어울리는 관계 |
|---|---|---|
RESTRICT (기본값) | 자식이 있으면 삭제 거부 | 대부분의 경우 — 가장 안전 |
CASCADE | 자식도 같이 삭제 | ”같이 사라져야 정상” — 게시글과 댓글 |
SET NULL | 자식의 FK 컬럼을 NULL 로 | ”부모 없어도 자식은 남아야” — 부서와 직원 |
-- 게시글이 지워지면 그 댓글도 같이 삭제
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
-- 부서가 없어져도 직원은 남기고, 부서ID 만 NULL 로
FOREIGN KEY (department_id) REFERENCES departments(department_id) ON DELETE SET NULL지금은 그냥 기본값 RESTRICT 그대로 두세요. 자식이 있으면 부모 삭제를
거부 — 데이터를 잃을 위험이 없는 가장 안전한 정책입니다. 나머지 둘은 “이런
선택지가 있다” 정도만 이해해두면 충분해요.
실무에서는 FK 를 안 쓴다던데요?
가끔 “우리 회사는 FK 안 써요” 라는 곳도 있습니다. 이유는 크게 두 가지예요.
- 성능 — INSERT/UPDATE 마다 부모가 존재하는지 매번 검증하니, 쓰기가 극단적으로 많은 시스템에서는 부담이 될 수 있습니다.
- 마이그레이션 편의 — 대량 데이터를 옮길 때 FK가 적재 순서를 강제해서 방해가 되기도 합니다.
“성능 때문에 FK를 빼야 한다” 는 건 데이터가 수억 건을 넘어가는 매우 특수한 단계의 이야기입니다. 학습 단계와 일반적인 실무에서는 — FK를 항상 쓰는 게 정답. 유령 데이터가 잡아먹는 비용이 FK 검증 비용보다 훨씬 큽니다.
AI 시대의 FK — 정확한 JOIN 경로 지도
2-4에서 만든 AI 과외 선생님이 user_id 가 아니라 member_id 로 정확히 답했던 것, 기억하시나요? init.sql 안의 FK 정의 덕분입니다. AI에게 SQL을 시킬 때 FK 정보만 같이 넘겨줘도, AI가 JOIN 의 ON 조건을 추측 없이 정확히 잡습니다.
[FK 정보]
orders.member_id → members.member_id
order_items.order_id → orders.order_id
order_items.product_id → products.product_id
products.category_id → categories.category_id이 네 줄이 shop DB 의 모든 JOIN 경로입니다. 섹션 5에서 AI에게 컨텍스트를 설계해주는 법을 배울 때, 이 FK 블록이 핵심 재료가 됩니다.
자주 막히는 지점
17~20번 회원(황태웅·안소영·류재민·백서윤)은 주문이 한 건도 없습니다 — 자식 없는 부모라서 삭제가 허용됩니다.
실제 에러에는 DB명·테이블명·제약 정의가 길게 붙지만, 읽어야 할 부분은 두 곳뿐입니다.
a foreign key constraint fails 한 구절 + 괄호 안의 제약 이름(fk_orders_member) — 어떤 FK가 막았는지 알려줍니다.위반 실험은 전부 거부되므로 애초에 데이터가 바뀌지 않습니다. 마음껏 깨뜨려보세요.
DROP DATABASE IF EXISTS 라서 — Alt + X 한 번이면 언제든 처음 상태로 돌아옵니다.관계를 그리는 법(4-1~4-3), 그리고 지키는 법(4-4)까지 끝났습니다. 다음은 실무에서 ERD 한 장을 받았을 때 그 설계도를 읽어내는 눈 — 방금 배운 FK가 ERD에서 테이블을 잇는 ‘선’ 이 됩니다.