정규화 — 중복이 만드는 이상(anomaly)을 없애기
테이블을 대충 넓게 만들면 — 한 테이블에 회원·주문·상품을 다 욱여넣으면 — 당장은 편하다. 그런데 같은 사실이 여러 행에 중복되는 순간, 세 가지 *이상 현상(anomaly)*이 생긴다. 정규화는 *함수 종속(functional dependency)*을 따져 테이블을 쪼개서 이 이상을 없애는 작업이다. 핵심 직관 한 줄 — "한 테이블에는 한 가지 주제(엔터티)만, 모든 비키 속성은 오직 기본키에만 의존하게." (출처: Silberschatz DBSC 7e Ch 7 / 김연희 데이터베이스 개론 정규화 장.)
왜 쪼개나 — 세 가지 이상 현상
주문(회원ID, 회원명, 회원등급, 상품명, 가격)처럼 여러 주제를 한 표에 두면:
- 삽입 이상(insertion) — 주문하지 않은 신규 회원을 넣고 싶은데, 주문 행이 없어 못 넣는다(상품 칸을 NULL로 둬야 함).
- 갱신 이상(update) — 회원 등급이 바뀌면 그 회원의 모든 주문 행을 다 고쳐야 하고, 하나라도 빠지면 데이터가 모순된다.
- 삭제 이상(deletion) — 마지막 주문을 지우면 회원 정보까지 같이 사라진다.
눈으로 보면 더 분명하다. 한 표에 회원·등급·상품이 섞인 비정규 테이블:
| 주문ID | 회원ID | 회원등급 | 상품명 | 가격 |
|---|---|---|---|---|
| 1 | 42 | GOLD | 키보드 | 50000 |
| 2 | 42 | GOLD | 마우스 | 30000 |
| 3 | 99 | SILVER | 키보드 | 50000 |
회원 42의 등급(GOLD)·상품 가격(키보드 50000)이 여러 행에 중복된다. 42가 PLATINUM이 되면 행 1·2를 다 고쳐야(갱신 이상), 행 3을 지우면 키보드 가격 정보가 사라지고(삭제 이상), 주문 안 한 신규 회원은 넣을 곳이 없다(삽입 이상).
이 세 이상의 공통 원인은 중복이고, 중복의 원인은 한 테이블에 독립적인 사실 여럿이 섞인 것이다. 정규화는 이걸 함수 종속이라는 도구로 정밀하게 분해한다 — 위 표는 주문(주문ID, 회원ID) · 회원(회원ID, 등급) · 상품(상품명, 가격)으로 쪼개면 각 사실이 정확히 한 곳에만 남는다.
함수 종속 — "X를 알면 Y가 정해진다"
X → Y (X가 Y를 함수적으로 결정)는 *"X 값이 같으면 Y 값도 반드시 같다"*는 뜻이다. 회원ID → 회원명은 성립(같은 회원ID면 이름 동일), 회원명 → 회원ID는 보통 불성립(동명이인). 정규화는 "모든 비키 속성이 기본키 전체에 완전히, 그리고 직접 종속되게" 만드는 과정이다.
1NF — 원자 값 (칸 하나에 값 하나)
제1정규형은 *모든 속성이 원자값(atomic)*이어야 한다 — 한 칸에 "사과,배,감"처럼 여러 값을 쉼표로 넣거나, 전화1·전화2·전화3처럼 반복 그룹을 두면 위반이다. 해법은 값을 행으로 펼치거나 별도 테이블로 분리.
PostgreSQL은 배열·JSONB 타입을 지원해 "한 칸에 여러 값"이 물리적으로는 가능하지만, 관계 모델 관점의 1NF는 여전히 "검색·조인·제약의 단위가 원자인가"를 묻는다. 배열은 편의일 뿐, 조인 대상이 되는 값은 별도 행/테이블로 두는 게 정석이다.
2NF — 부분 종속 제거 (복합키일 때만 문제)
제2정규형은 1NF + 부분 함수 종속 제거다. *복합 기본키(예: (주문ID, 상품ID))*가 있을 때, 키의 일부에만 의존하는 비키 속성이 있으면 위반이다.
주문상품(주문ID, 상품ID, 주문일, 상품명, 수량)
(주문ID, 상품ID) → 수량 ✅ 키 전체에 의존
주문ID → 주문일 ❌ 키의 '일부'에만 의존 (부분종속)
상품ID → 상품명 ❌ 부분종속
주문일은 주문ID만으로 정해지므로 주문 테이블로, 상품명은 상품 테이블로 빼낸다. 단일 기본키(대리키 하나)면 부분 종속이 애초에 불가능하므로 2NF는 자동 충족된다 — 그래서 대리키를 쓰면 2NF 고민이 줄어든다.
3NF — 이행 종속 제거 (비키 → 비키 금지)
제3정규형은 2NF + 이행 종속(transitive) 제거다. 비키 속성이 다른 비키 속성을 거쳐 키에 의존하면 위반이다.
회원(회원ID, 등급코드, 등급명)
회원ID → 등급코드 ✅
등급코드 → 등급명 ❌ 비키 → 비키 (이행종속: 회원ID → 등급코드 → 등급명)
등급명은 회원ID에 직접이 아니라 등급코드를 거쳐 의존한다. 등급(등급코드, 등급명) 테이블로 분리하면, 등급명을 바꿀 때 한 곳만 고치면 된다(갱신 이상 해소). 실무 정규화의 목표 지점이 보통 3NF다 — "모든 비키 속성은 키, 키 전체, 오직 키에만 의존(the key, the whole key, nothing but the key)."
BCNF — 3NF의 빈틈을 메운 강한 형태
**BCNF(Boyce-Codd)**는 *"모든 결정자(X → Y의 X)가 후보키여야 한다"*는 더 엄격한 형. 3NF인데 BCNF가 아닌 드문 경우는 후보키가 여럿이고 서로 겹칠 때 생긴다(예: (학생, 과목) → 교수, 교수 → 과목). 여기서 교수는 결정자인데 후보키가 아니므로 BCNF 위반. 분해하면 해소되지만 — 주의: BCNF 분해는 때로 함수 종속 보존(dependency preservation)을 깨 무손실이지만 일부 제약을 조인 없이 검사 못 하게 만들 수 있다. 그래서 3NF는 무손실+종속보존을 항상 보장하지만 BCNF는 무손실은 보장하되 종속보존은 못 할 수 있다 — 이 트레이드오프가 시험·면접 단골이다.
4NF·5NF — 더 드문 다치/조인 종속
- 4NF — 다치 종속(multivalued dependency) 제거. 서로 독립인 다중값 두 개를 한 테이블에 두면(예: 한 사람의
취미들과자격증들) 불필요한 곱이 생긴다. 둘을 별도 테이블로. - 5NF(PJ/NF) — 조인 종속 제거. 세 개 이상으로 쪼개야만 무손실이 되는 매우 드문 경우.
실무에서 4NF·5NF까지 의식적으로 가는 일은 적지만, *"독립적인 다중값은 한 표에 같이 두지 않는다"*는 직관은 4NF의 핵심이다.
반정규화 — 일부러 거꾸로 가기
정규화는 중복을 없애 무결성을 지키지만, 조인이 많아져 읽기가 느려질 수 있다. 그래서 읽기 성능이 절실한 곳에선 의도적으로 반정규화(denormalization) — 집계 칼럼 캐싱(order_count)·중복 칼럼·요약 테이블을 둔다. 단, 반정규화는 갱신 이상을 되살리는 거래다 — 중복된 값의 동기화 책임을 (트리거·배치·애플리케이션이) 져야 한다. 원칙은 "먼저 정규화(3NF)로 설계하고, 측정해서 느린 곳만 신중히 반정규화." 처음부터 성능을 핑계로 넓은 테이블을 만드는 건 조기 최적화다.
한 줄 요약 — 이상 현상의 원인은 중복, 중복의 원인은 잘못된 함수 종속. 1NF(원자값)→2NF(부분종속)→3NF(이행종속)→BCNF(결정자=후보키) 순으로 쪼갠다. 실무 목표는 3NF, 성능이 절실하면 측정 후 반정규화.
(출처: Silberschatz DBSC 7e Ch 7 Functional Dependencies & Normalization / 김연희 데이터베이스 개론 / PostgreSQL — Data Types(array, jsonb).)