반정규화 (Denormalization)
핵심 개념
정규화된 테이블을 성능을 위해 의도적으로 중복시키는 것.
[정규화 상태]
orders: id, user_id, total
users: id, name
→ 주문 조회 시 항상 JOIN 필요
SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
[반정규화 후]
orders: id, user_id, user_name, total
↑ 중복 저장!
→ JOIN 없이 바로 조회
SELECT * FROM orders WHERE id = 1
정규화 vs 반정규화
| 항목 | 정규화 | 반정규화 |
|---|
| 중복 | 없음 | 의도적 중복 |
| 쓰기 | 빠름 | 느림 (여러 곳 업데이트) |
| 읽기 | 느림 (JOIN) | 빠름 (JOIN 제거) |
| 정합성 | 보장 | 불일치 위험 |
| 적합 | OLTP (쓰기 중심) | OLAP (읽기 중심) |
반정규화 기법
1. 컬럼 추가 (가장 흔함)
orders에 user_name 추가
2. 파생 컬럼 추가
users에 order_count 추가
(매번 COUNT 쿼리 대신)
3. 테이블 병합
1:1 관계 테이블 합치기
4. 요약 테이블
daily_sales 집계 테이블 별도 생성
실무 적용 사례
반정규화가 적합한 경우
- 대시보드 집계 (일별 매출)
- 검색 결과 목록 (JOIN 제거)
- 읽기 비율 >> 쓰기 비율
반정규화를 피해야 할 경우
- 자주 변경되는 데이터
- 정합성이 중요한 금융 데이터
- 쓰기가 많은 시스템
정합성 유지 방법
| 방법 | 설명 |
|---|
| 트리거 | 원본 변경 시 자동 동기화 |
| 이벤트 기반 | 변경 이벤트 → 비동기 업데이트 |
| 배치 처리 | 주기적 동기화 (집계 테이블) |
Q.반정규화는 언제 필요하고, 어떤 위험이 있나요?
조회 성능이 병목이고 정규화된 구조로는 감당이 안 될 때입니다. 다만 정합성 유지 비용을 함께 집니다.
| 언제 | 예 |
|---|
| 조인이 많아 조회가 느리다 | 게시글 목록에 작성자 이름을 함께 둔다 |
| 집계를 매번 계산하기 비싸다 | 댓글 수를 컬럼으로 둔다 |
| 읽기가 쓰기보다 압도적으로 많다 | 대시보드, 상품 목록 |
| 위험 | 내용 |
|---|
| 갱신 이상 | 원본이 바뀌었는데 사본이 그대로 남는다 |
| 갱신 비용 증가 | 한 값을 고치려고 여러 곳을 고쳐야 한다 |
| 코드 복잡도 | 어디를 함께 고쳐야 하는지 사람이 기억해야 한다 |
흔한 실수: 측정 없이 미리 반정규화하는 것. 실행 계획을 보고 조인이 실제 병목인지 확인한 뒤에 해야 합니다. 인덱스로 해결되는 경우가 훨씬 많습니다.
Q.반정규화 후 데이터 정합성은 어떻게 유지하나요?
사본을 언제 어떻게 갱신할지 정하고, 어긋남을 감지할 장치를 둡니다.
| 방법 | 내용 | 특징 |
|---|
| 같은 트랜잭션에서 함께 갱신 | 원본과 사본을 한 번에 | 정확하지만 쓰기가 무거워진다 |
| 변경 이벤트로 갱신 | 원본 변경을 구독해 사본을 고친다 | 결합이 낮다. 잠깐 어긋난다 |
| DB 트리거 | DB 가 자동으로 사본을 고친다 | 코드에 안 보여 추적이 어렵다 |
| 주기적 재계산 | 배치로 다시 맞춘다 | 그 사이 어긋난 채로 남는다 |
무엇을 쓰든 정합성 점검을 정기적으로 돌리는 것이 안전합니다. 원본과 사본을 비교해 다른 건수를 알림으로 보내면 조용히 틀어지는 것을 막습니다.
흔한 실수: 갱신 경로를 하나로 가정하는 것. 관리자 화면, 배치, 데이터 보정 스크립트처럼 사본을 모르는 경로가 늘 생깁니다. 그래서 감지 장치가 필요합니다.
Q.읽기 성능 최적화를 위해 반정규화 외에 어떤 방법이 있나요?
반정규화는 정합성 비용이 크므로 대개 마지막에 봅니다.
| 방법 | 언제 |
|---|
| 인덱스 추가와 정비 | 대부분의 느린 조회가 여기서 해결된다 |
| 커버링 인덱스 | 테이블 접근을 없앤다 |
| 쿼리 재작성 | 함수 적용 제거, 불필요한 조인 제거, 페이지네이션 방식 변경 |
| 캐시 | 반복 조회가 많고 약간의 지연을 허용할 때 |
| 읽기 복제본 | 조회 트래픽을 분산한다 |
| 구체화 뷰 | 집계를 미리 계산해 둔다 |
| 파티셔닝 | 큰 테이블에서 스캔 범위를 줄인다 |
순서가 있습니다. 먼저 재보고, 인덱스와 쿼리로 풀리는지 확인한 뒤, 캐시와 복제본을 검토하고, 그래도 안 되면 구조를 바꿉니다.
흔한 실수: 캐시부터 붙이는 것. 원인이 풀 스캔이면 캐시가 미스일 때 그대로 느리고, 무효화 문제까지 새로 떠안습니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
데이터베이스 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.