정규화와 반정규화 — 1NF부터 3NF, 그리고 언제 비정규화하는가
테이블을 어떻게 나눠야 "잘 설계한 DB"라고 할 수 있을까?
데이터베이스 설계에서 가장 기본이 되는 개념이 정규화입니다. 공부하다 보니 정규화 자체는 어렵지 않은데, "언제 정규화하고 언제 안 하는가" 의 판단 기준이 실제로 중요한 포인트였습니다.
정규화란
데이터 중복을 최소화하고, 이상 현상(Anomaly)을 방지하기 위해 테이블을 분리하는 과정
이상 현상이란:
- **삽입 이상 **: 불필요한 데이터를 함께 넣어야 삽입 가능
- ** 갱신 이상 **: 중복 데이터 중 일부만 수정 → 데이터 불일치
- ** 삭제 이상 **: 데이터 삭제 시 의도치 않은 다른 정보까지 사라짐
제1정규형 (1NF) — 원자성
모든 컬럼의 값이 ** 원자값(Atomic Value)**이어야 한다.
하나의 셀에 여러 값이 들어가면 안 됩니다.
❌ 1NF 위반
| 학생ID | 이름 | 수강과목 |
|--------|--------|-------------------|
| 1 | 김철수 | 수학, 영어, 과학 |
✅ 1NF 만족
| 학생ID | 이름 | 수강과목 |
|--------|--------|----------|
| 1 | 김철수 | 수학 |
| 1 | 김철수 | 영어 |
| 1 | 김철수 | 과학 |
실무에서 "콤마로 구분된 문자열"을 컬럼에 넣는 건 전형적인 1NF 위반입니다. 검색도 어렵고 수정도 번거롭습니다.
제2정규형 (2NF) — 부분 함수 종속 제거
1NF를 만족하면서, ** 기본키의 일부에만 종속되는 컬럼 **을 분리한다.
복합 기본키가 있을 때 발생하는 문제입니다.
❌ 2NF 위반 — 기본키: (학생ID, 과목ID)
| 학생ID | 과목ID | 학생이름 | 성적 |
|--------|--------|----------|------|
| 1 | 101 | 김철수 | A |
→ "학생이름"은 학생ID만으로 결정됨 (과목ID와 무관)
→ 부분 함수 종속 발생
✅ 2NF 만족 — 테이블 분리
[학생] 학생ID → 학생이름
[수강] (학생ID, 과목ID) → 성적
기본키가 단일 컬럼이면 2NF는 자동으로 만족됩니다. 복합키일 때만 신경 쓰면 됩니다.
제3정규형 (3NF) — 이행 함수 종속 제거
2NF를 만족하면서, ** 기본키가 아닌 컬럼이 다른 비키 컬럼을 결정하는 관계 **를 제거한다.
❌ 3NF 위반
| 학생ID | 학과 | 학과장 |
|--------|--------|----------|
| 1 | 컴공 | 이교수 |
| 2 | 컴공 | 이교수 |
→ 학생ID → 학과 → 학과장 (이행 함수 종속)
→ 학과장이 바뀌면 모든 행을 수정해야 함 (갱신 이상)
✅ 3NF 만족 — 테이블 분리
[학생] 학생ID → 학과
[학과] 학과 → 학과장
BCNF (Boyce-Codd Normal Form)
모든 결정자가 후보키여야 한다.
3NF보다 조금 더 엄격한 형태입니다. 실무에서 3NF와 BCNF의 차이가 문제되는 경우는 드물지만, 알아두면 좋습니다.
❌ BCNF 위반 예시
| 학생 | 과목 | 교수 |
|------|--------|--------|
| A | DB | 이교수 |
| B | DB | 김교수 |
→ 교수 → 과목 (교수가 결정자인데 후보키가 아님)
왜 실무에서 3NF까지만 하는가
4NF, 5NF도 존재하지만 실무에서는 거의 3NF(또는 BCNF)까지만 적용합니다.
- 4NF 이상은 다치 종속, 조인 종속 등 복잡한 케이스를 다룸
- 과도한 정규화 → 테이블이 너무 많아져 JOIN 비용 증가
- 대부분의 이상 현상은 3NF로 해결됨
반정규화 (Denormalization)
** 의도적으로 중복을 허용 **하여 읽기 성능을 높이는 기법
정규화의 반대가 아니라, ** 정규화를 먼저 한 뒤** 성능상 필요한 부분만 선택적으로 되돌리는 것입니다.
주요 반정규화 기법
1. 중복 컬럼 추가
-- 정규화된 상태: 주문 조회 시 매번 고객 테이블 JOIN
SELECT o.*, c.name FROM orders o JOIN customers c ON o.customer_id = c.id;
-- 반정규화: 자주 조회하는 고객명을 주문 테이블에 추가
ALTER TABLE orders ADD COLUMN customer_name VARCHAR(100);
2. 계산 컬럼 추가
-- 매번 SUM 집계하는 대신
ALTER TABLE orders ADD COLUMN total_amount DECIMAL(10,2);
-- 주문 항목 변경 시 트리거나 애플리케이션 로직으로 갱신
3. 테이블 병합
-- 1:1 관계인 두 테이블을 하나로 합침
-- user + user_profile → user (프로필 컬럼 포함)
4. 테이블 분할 (파티셔닝)
- 수직 분할: 자주 쓰는 컬럼과 안 쓰는 컬럼 분리
- 수평 분할: 데이터를 날짜나 범위로 분리
OLTP vs OLAP에서의 차이
| 구분 | OLTP | OLAP |
|---|---|---|
| 목적 | 트랜잭션 처리 (주문, 결제) | 분석, 리포팅 |
| 작업 비율 | 쓰기 > 읽기 | 읽기 >> 쓰기 |
| 정규화 | 3NF 정규화 유지 | ** 반정규화** (스타/스노우플레이크 스키마) |
| 이유 | 데이터 무결성, 이상 현상 방지 | JOIN 비용 절감, 조회 성능 |
업계 표준은 "OLTP는 정규화, OLAP는 반정규화" 입니다.
실전 판단 기준
정규화와 반정규화 사이에서 어떻게 결정할까요?
정규화를 유지하는 경우
- 쓰기가 빈번한 트랜잭션 시스템
- 데이터 무결성이 최우선 (금융, 의료)
- ACID 보장이 필수
- 정규화만으로 쓰기 성능이 약 20% 향상될 수 있음
반정규화를 고려하는 경우
- 읽기 위주 시스템 (대시보드, 통계)
- 복잡한 JOIN이 성능 병목
- 응답 속도가 최우선 (사용자 대면 API)
- 반정규화로 읽기 성능이 최대 100% 향상될 수 있음
자주 하는 실수
- **처음부터 반정규화로 설계 **: 나중에 요구사항이 바뀌면 데이터 정합성 문제 폭발
- ** 모든 테이블을 과도하게 정규화 **: JOIN 10개짜리 쿼리가 나오면 유지보수가 어려움
- ** 반정규화 후 동기화 로직을 빠뜨림 **: 중복 데이터가 서로 달라지는 문제
정리
- 정규화는 데이터 중복을 제거하고 이상 현상을 방지하는 과정
- 1NF(원자성) → 2NF(부분 함수 종속 제거) → 3NF(이행 함수 종속 제거)
- 실무에서는 3NF까지면 대부분 충분
- 반정규화는 정규화를 먼저 한 뒤, 읽기 성능이 필요한 곳에 선택적으로 적용
- OLTP = 정규화, OLAP = 반정규화가 업계 표준
정규화가 먼저이고, 반정규화는 "측정된 성능 문제"에 대한 해결책입니다. 감으로 반정규화하면 나중에 데이터 정합성 지옥을 경험하게 됩니다.