조회 성능을 위해 의도적으로 중복을 허용하는 설계. 정규화를 되돌리는 것이다.
흔한 방법
| 방법 | 예 | 없애는 비용 |
|---|---|---|
| 컬럼 중복 | 주문에 회원이름 복사 | 회원 테이블 조인 |
| 집계 컬럼 | 게시글에 comment_count | COUNT(*) 쿼리 |
| 요약 테이블 | 일별 매출 미리 계산 | 대량 집계 |
| 테이블 병합 | 항상 함께 읽는 1:1 테이블 합치기 | 조인 |
| 컬럼 분리 | 큰 TEXT 컬럼을 별도 테이블로 | 불필요한 I/O |
반드시 따라오는 책임 — 정합성
원본이 바뀌면 복사본도 갱신해야 한다. 이걸 놓치면 화면마다 값이 달라진다.
- 회원이 이름을 바꿈
- 회원 테이블 UPDATE
- 주문 테이블의 회원이름 컬럼도 전부 UPDATE ← 빠뜨리면 옛 이름이 남는다
관리 방법은 셋 중 하나다.
- 갱신 지점을 한 곳으로 — 도메인 이벤트나 서비스 계층에서 함께 처리
- 정합성 검증 배치 — 주기적으로 대조해 어긋난 것을 고침
- 의도적으로 옛 값을 유지 — 주문 시점의 이름·가격을 스냅샷으로 남기는 것이 오히려 올바른 경우가 있다(주문 이력은 당시 정보여야 한다)
3번은 반정규화가 아니라 도메인 요구사항이다. 이 구분을 하면 좋은 답변이 된다.
판단 순서 — 반정규화는 마지막
- ① 실행계획으로 진짜 병목인지 확인
- ② 인덱스로 해결되는가
- ③ 쿼리를 고쳐 해결되는가
- ④ 캐시로 해결되는가 ← 되돌리기 쉬움
- ⑤ 그래도 안 되면 반정규화 ← 스키마 변경, 되돌리기 어려움
캐시를 먼저 시도하는 이유 — 캐시는 지우면 원상복구되지만, 반정규화는 스키마와 코드가 얽혀 되돌리기 어렵다.
면접 함정
"조인이 많으면 무조건 느리다" → 아니다. 인덱스가 잘 걸린 조인은 매우 빠르다. 느린 것은 조인 자체가 아니라 인덱스를 못 타거나 결과 집합이 큰 경우다. 실행계획으로 확인하지 않고 반정규화부터 하는 것이 흔한 실수다.