
데이터베이스 설계를 배울 때는 정규화를 하라고 배운다. 그런데 실무에서 성능 문제를 만나면 "비정규화하라"는 조언을 듣는다. 둘은 반대 방향인데 어느 쪽이 맞는가. 이 글은 정규화가 무엇을 막아주는지, 그리고 그것을 포기해도 되는 조건이 무엇인지를 정리한다.
정규화가 막는 것은 모순이다
정규화의 목적은 저장 공간 절약이 아니다. 같은 사실이 여러 곳에 적히는 것을 막는 것이다.
주문 테이블에 고객 이름을 직접 넣었다고 하자. 고객이 이름을 바꾸면 그 고객의 모든 주문 행을 고쳐야 한다. 하나라도 빠뜨리면 같은 고객인데 이름이 다른 데이터가 생긴다. 어느 쪽이 맞는지 알 수 없다.
이걸 이상 현상(anomaly)이라고 부르고 셋으로 나눈다.
갱신 이상. 위 경우처럼 일부만 고쳐져 모순이 생긴다. 삽입 이상. 아직 주문한 적 없는 고객을 등록할 수 없다. 주문 행이 있어야만 고객 정보가 존재하기 때문이다. 삭제 이상. 마지막 주문을 지우면 고객 정보까지 사라진다.
정규화는 사실을 한 곳에만 적어서 이 셋을 구조적으로 없앤다. 고객 이름은 고객 테이블에만 있고, 주문은 고객 ID로 참조한다.

3정규형까지가 실무 기준
정규형은 여러 단계가 있지만 실무에서는 대체로 3정규형(3NF)까지 맞추면 충분하다.
간단히 말하면 이렇다. 모든 컬럼이 기본 키에 의존하고(2NF), 키가 아닌 컬럼끼리 의존하지 않아야 한다(3NF). 예를 들어 주문 테이블에 우편번호와 도시가 함께 있으면, 도시는 우편번호에 의존하므로 3NF 위반이다.
BCNF 이상은 학술적으로는 의미가 있지만 실무에서 문제가 되는 경우가 드물다. 3NF까지 맞춰 놓고 실제 문제가 생기면 그때 더 보면 된다.
비정규화는 측정한 뒤에 한다
정규화된 스키마는 조인이 많아진다. 조인이 늘면 느려진다는 것이 비정규화의 근거다. 그런데 여기에는 전제가 붙는다.
조인 자체는 생각보다 싸다. 인덱스가 제대로 걸린 조인은 밀리초 단위로 끝난다. 느린 원인이 조인이 아니라 인덱스 부재나 N+1인 경우가 훨씬 많다. 비정규화하기 전에 실행 계획부터 봐야 한다.
그럼에도 비정규화가 정당한 경우가 있다.
과거 시점의 값을 보존해야 할 때. 주문 시점의 상품 가격은 정규화 관점에서는 상품 테이블을 참조해야 하지만, 그러면 가격이 바뀔 때 과거 주문 금액까지 바뀐다. 주문 행에 당시 가격을 복사해 두는 것이 맞다. 이건 중복이 아니라 다른 사실이다. 현재 가격과 주문 당시 가격은 별개의 정보다.
읽기가 압도적으로 많고 조인이 깊을 때. 목록 화면에서 다섯 테이블을 조인해야 한다면, 자주 쓰는 값을 복사해 두는 것이 실용적일 수 있다.
집계 값을 매번 계산하기 비쌀 때. 게시글의 댓글 수를 매번 COUNT하는 대신 카운터 컬럼을 둔다.
비정규화의 대가를 어떻게 감당하나
복사해 둔 값은 원본이 바뀔 때 같이 갱신돼야 한다. 이걸 어떻게 보장할지가 설계의 핵심이다.
같은 트랜잭션에서 함께 갱신. 가장 확실하다. 댓글을 추가할 때 카운터도 같은 트랜잭션에서 올린다. 다만 경합이 심해질 수 있다.
데이터베이스 트리거. 애플리케이션이 잊어도 데이터베이스가 보장한다. 다만 로직이 코드 밖에 숨어서 디버깅이 어렵다.
비동기 갱신. 이벤트를 발행하고 나중에 맞춘다. 성능은 좋지만 잠시 어긋난 상태가 생긴다. 그 시간 동안 틀린 값이 보여도 괜찮은지 판단해야 한다.
주기적 재계산. 배치로 정합성을 맞춘다. 위 방법들과 함께 쓰는 안전망으로 유용하다. 어긋난 데이터가 얼마나 있는지 세어보는 것만으로도 문제를 조기에 발견한다.
어느 방법을 쓰든 어긋날 수 있다는 것을 전제로 설계해야 한다. 정합성 검증 쿼리를 만들어 두고 주기적으로 돌리는 것을 권한다.
순서를 지키면 후회가 적다
정리하면 내 기준은 이렇다.
먼저 정규화한다. 설계 단계에서는 3NF를 목표로 한다. 정규화된 스키마는 요구사항이 바뀔 때 유연하다. 비정규화된 스키마를 나중에 정규화하는 것은 훨씬 어렵다.
느려지면 원인을 측정한다. 실행 계획을 보고 인덱스와 쿼리 패턴을 먼저 고친다. 대부분 여기서 해결된다.
그래도 안 되면 비정규화한다. 이때도 어디를 왜 비정규화했는지 주석과 문서에 남긴다. 나중에 보는 사람이 "이거 실수 아닌가" 하고 고치지 않도록.
갱신 책임을 명시한다. 복사된 값을 누가 언제 갱신하는지 코드 한 곳에 모은다. 여러 곳에서 제각각 갱신하면 언젠가 어긋난다.
마지막으로 하나. 요즘은 읽기 전용 복제본이나 검색 엔진, 전용 집계 테이블처럼 원본 스키마를 건드리지 않고 읽기를 빠르게 하는 선택지가 많다. 목록 조회가 느리다고 바로 주 테이블을 비정규화하기보다, 조회 전용 뷰를 따로 만드는 편이 나은 경우가 많다. 쓰기 모델과 읽기 모델을 나누면 각자 최적의 모양을 가질 수 있다.
참고 자료
- Database normalization — PostgreSQL Wiki — 흔한 스키마 설계 실수 모음
- PostgreSQL: Using EXPLAIN — 비정규화 전에 원인을 측정하는 도구
- MySQL: Optimizing Database Structure — 스키마 설계가 성능에 미치는 영향
'IT' 카테고리의 다른 글
| HTTP 상태 코드와 오류 응답 설계 (0) | 2026.10.26 |
|---|---|
| 알림 피로를 줄이는 SLO 기반 모니터링 (0) | 2026.10.25 |
| 환경 변수와 시크릿 관리, 어디까지 해야 하나 (0) | 2026.10.24 |
| 메시지 큐를 언제 써야 하고 언제 쓰면 안 되나 (0) | 2026.10.23 |
| 트랜잭션 격리 수준과 데드락 줄이기 (0) | 2026.10.22 |
