8~9강에서 정규화를 했다면, 이번 편은 근거를 갖고 되돌리는 방법입니다.
1. 반정규화란
정규화된 모델에 의도적으로 중복이나 구조 변경을 도입해 성능이나 단순성을 얻는 것입니다. 핵심은 "정규화를 안 한 것"과 "정규화한 뒤 근거를 갖고 되돌린 것"이 다르다는 점입니다.
2. 반정규화를 검토하기 전에
순서대로 확인합니다.
반정규화는 모든 쓰기 경로에 비용을 추가하기 때문에 마지막 수단입니다.
3. 반정규화 기법
| 분류 | 기법 | 예시 |
|---|---|---|
| 컬럼 | 중복 컬럼 | fulfillment_log에 order_no 추가 (주문항목 조인 제거) |
| 컬럼 | 파생 컬럼 | order_item에 progress_pct 저장 |
| 컬럼 | 이력 최신값 컬럼 | order_item에 last_stage_code 저장 |
| 테이블 | 집계 테이블 | 일별 주문 단위 처리 집계 |
| 테이블 | 테이블 병합 | 1:1 테이블 합치기 |
| 테이블 | 수직 분할 | 자주 안 쓰는 대형 컬럼 분리 |
| 테이블 | 수평 분할 | 최근 데이터와 과거 데이터 분리 (파티셔닝으로 대체 가능) |
| 관계 | 중복 관계 | 손자 테이블이 조부모 FK를 직접 보유 |
4. 정합성 보장 수단 비교
반정규화의 진짜 설계 대상은 "중복을 어떻게 동기화할 것인가" 입니다.
| 수단 | 일관성 | 쓰기 비용 | 복잡도 | 적합한 경우 |
|---|---|---|---|---|
| 생성 컬럼 (Generated Column) | 즉시, DB 보장 | 낮음 | 낮음 | 같은 행 내 계산 |
| 복합 FK + ON UPDATE CASCADE | 즉시, DB 보장 | 중간 | 낮음 | 부모 값 복사 (8강 예제) |
| 트리거 | 즉시, DB 보장 | 높음 | 높음 (숨은 로직) | 다른 테이블 집계 |
| 애플리케이션 | 코드 품질에 의존 | 중간 | 중간 | 트랜잭션 경계가 명확할 때 |
| 구체화 뷰 (Materialized View) | 새로 고침 시점 | 새로 고침 시 | 낮음 | 지연 허용 집계 |
| 배치 | 배치 주기 | 배치 시 | 중간 | 대시보드, 리포트 |
| CDC / 이벤트 | 수 초 지연 | 비동기 | 높음 | 시스템 간 복제 |
5. 예제 1: 생성 컬럼
-- PostgreSQL 12+
ALTER TABLE item_stage
ADD COLUMN delay_days INT
GENERATED ALWAYS AS (actual_date - plan_date) STORED;
-- MySQL 5.7+
ALTER TABLE item_stage
ADD COLUMN delay_days INT
GENERATED ALWAYS AS (DATEDIFF(actual_date, plan_date)) STORED;같은 행에서 계산되는 파생값은 생성 컬럼이 가장 안전하며, 인덱스도 걸 수 있습니다.
6. 예제 2: 중복 컬럼과 복합 FK
처리 실적을 주문 단위로 집계하려면 fulfillment_log → order_item → orders 조인이 필요합니다. 실적이 수천만 건이면 부담이 큽니다.
ALTER TABLE order_item ADD CONSTRAINT uq_item_order UNIQUE (item_id, order_no);
ALTER TABLE fulfillment_log ADD COLUMN order_no VARCHAR(10);
-- 기존 데이터 채운 뒤
ALTER TABLE fulfillment_log ALTER COLUMN order_no SET NOT NULL;
ALTER TABLE fulfillment_log
ADD CONSTRAINT fk_log_item_order
FOREIGN KEY (item_id, order_no) REFERENCES order_item (item_id, order_no)
ON UPDATE CASCADE;order_no는 중복이지만, 복합 FK 덕분에 주문항목의 주문과 다른 값이 들어갈 수 없습니다. 트리거 없이 DB가 정합성을 보장하는 중복입니다.
7. 예제 3: 집계 테이블
CREATE TABLE daily_order_progress (
base_date DATE NOT NULL,
order_no VARCHAR(10) NOT NULL,
total_tasks INT NOT NULL,
done_tasks INT NOT NULL,
total_man_hours NUMERIC(12,2) NOT NULL,
refreshed_at TIMESTAMP NOT NULL,
PRIMARY KEY (base_date, order_no)
);배치로 갱신합니다. 이미 학습한 스테이징 적재 → MERGE/UPSERT 패턴이 그대로 적용됩니다.
INSERT INTO daily_order_progress AS t
(base_date, order_no, total_tasks, done_tasks, total_man_hours, refreshed_at)
SELECT CURRENT_DATE, oi.order_no,
COUNT(*), COUNT(st.actual_date),
COALESCE(SUM(w.mh), 0), now()
FROM order_item oi
JOIN item_stage st ON st.item_id = oi.item_id
LEFT JOIN (SELECT item_id, stage_code, SUM(man_hours) AS mh
FROM fulfillment_log GROUP BY item_id, stage_code) w
ON w.item_id = st.item_id AND w.stage_code = st.stage_code
GROUP BY oi.order_no
ON CONFLICT (base_date, order_no) DO UPDATE
SET total_tasks = EXCLUDED.total_tasks,
done_tasks = EXCLUDED.done_tasks,
total_man_hours = EXCLUDED.total_man_hours,
refreshed_at = EXCLUDED.refreshed_at;refreshed_at 컬럼으로 데이터가 언제 기준인지 사용자에게 노출하는 것이 집계 테이블 설계의 필수 요소입니다.
8. 반정규화 결정 기록
반정규화는 반드시 문서로 남깁니다. 1년 뒤에는 누구도 이유를 기억하지 못합니다.
| 항목 | 내용 |
|---|---|
| 대상 | fulfillment_log.order_no |
| 문제 | 주문 단위 실적 집계 쿼리 3.2초 (목표 0.5초) |
| 검토한 대안 | 인덱스 추가(효과 미미), 구체화 뷰(실시간성 부족) |
| 선택 | 중복 컬럼 + 복합 FK |
| 정합성 수단 | FK ON UPDATE CASCADE |
| 비용 | 행당 약 11바이트; order_no는 불변이라 연쇄 갱신은 발생하지 않는다 |
| 측정 결과 | 0.3초 |
10강 정리
- 반정규화는 인덱스·쿼리·캐시·파티셔닝 다음의 마지막 수단이다.
- 설계의 핵심은 중복 자체가 아니라 동기화 수단이다.
- 가능하면 생성 컬럼, 복합 FK처럼 DB가 보장하는 수단을 우선한다.
- 집계 테이블에는 기준 시각을 함께 저장한다.
- 결정 근거와 측정 결과를 기록한다.