정규화 — 중복이 만드는 이상(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).)