정규화를 어디까지 할 것인가 — 3NF 이후의 판단
정규화는 규칙이 아니라 트레이드오프다. 언제 비정규화가 정당화되는지의 기준.
정규화가 해결하는 문제
정규화의 목적은 중복 제거를 통한 이상(anomaly) 방지다.
같은 정보가 여러 곳에 있으면:
- 갱신 시 일부만 바뀌어 불일치 (갱신 이상)
- 마지막 행을 지우면 정보 자체가 사라짐 (삭제 이상)
- 일부 정보만으로는 행을 넣을 수 없음 (삽입 이상)
3NF까지 가면 이 세 문제의 대부분이 해소된다.
3NF까지의 요약
| 단계 | 조건 |
|---|---|
| 1NF | 각 칸에 원자값 하나 (배열·콤마 목록 없음) |
| 2NF | 1NF + 부분 함수 종속 제거 (복합 키의 일부에만 의존하는 컬럼 분리) |
| 3NF | 2NF + 이행 종속 제거 (키가 아닌 컬럼에 의존하는 컬럼 분리) |
실무에서 대부분의 스키마는 3NF를 목표로 시작하면 충분하다.
비정규화가 정당화되는 경우
정규화된 스키마의 대가는 조인이다. 조회 시 여러 테이블을 합쳐야 한다.
비정규화를 고려할 조건:
- 실측된 성능 문제가 있다 — 예상이 아니라 측정치
- 조회가 쓰기보다 압도적으로 많다
- 조인 대상이 거의 변하지 않는다
- 인덱스·쿼리 개선으로 해결되지 않았다
4번이 중요하다. 대부분의 조인 성능 문제는 인덱스나 쿼리 구조로 해결된다. 비정규화는 그다음이다.
비정규화의 형태
1. 컬럼 복제
자주 조인하는 값을 복사해 둔다.
```
orders.user_name -- users에서 복사
```
갱신 경로 관리가 필요하다. 사용자가 이름을 바꾸면 어떻게 할 것인가?
여기서 판단이 갈린다. 주문 시점의 이름을 보존해야 한다면 이건 비정규화가 아니라 의도적 스냅샷이다. 이 경우 오히려 정규화된 참조가 잘못된 설계다.
2. 집계 컬럼
```
posts.comment_count
```
매번 세는 대신 저장한다. 갱신 누락 시 값이 틀어지므로, 정기적으로 재계산하는 배치를 함께 두는 경우가 많다.
3. 조회 전용 테이블
원본은 정규화된 채로 두고, 조회용 구조를 별도로 만든다. 동기화 지연을 허용할 수 있을 때 쓴다.
관리 방법을 함께 정한다
비정규화를 결정했다면 다음을 문서로 남긴다.
```markdown
비정규화: orders.user_name
- 이유: 주문 목록 조회에서 users 조인이 응답의 60% 차지 (측정: N ms → M ms)
- 갱신 정책: 주문 시점 값 보존. 사용자가 이름을 바꿔도 갱신하지 않음
- 불일치 허용: 의도된 것 (주문 시점 기록)
- 재검토 조건: 주문 목록 조회 패턴이 바뀌면
```
갱신 정책과 불일치 허용 범위가 핵심이다. 이게 명시되지 않으면 나중에 "이 값이 왜 다르지"에서 며칠이 사라진다.
시작 시점의 선택
초기 설계에서는 정규화 쪽에서 시작하는 편이 안전하다. 이유는 성능이 아니라 변경 가능성이다.
- 정규화된 구조 → 비정규화로 가기 쉬움 (복제 컬럼 추가)
- 비정규화된 구조 → 정규화로 되돌리기 어려움 (데이터 정리 필요)
접근 패턴이 확정되기 전에 비정규화하면, 패턴이 바뀔 때 구조 전체를 손봐야 한다.
최종 수정 2026-08-28