반정규화
정규화가 데이터 무결성을 위한 분해라면, 반정규화(Denormalization)는 성능을 위한 의도적 역행입니다. 정규화만이 정답은 아닙니다 — 읽기 성능이 극도로 중요한 상황에서는 반정규화가 필요합니다. 반정규화는 무결성을 일부 희생하는 트레이드오프이므로, 언제·왜·어떻게 적용하는지 정확히 판단할 수 있어야 합니다.
반정규화가 필요한 상황
정규화된 테이블은 데이터 무결성이 높지만, 조인이 많아집니다. 조인은 비용이 큰 연산입니다.
"사용자별 주문과 상품명을 조회하라"
SELECT u.name, o.order_date, p.name, oi.quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
→ 4개 테이블 조인!
→ 데이터가 수천만 건이면 성능 저하
→ 대시보드에서 이 쿼리가 1초에 100번 호출된다면?| 반정규화를 고려해야 하는 신호 | 예시 |
|---|---|
| 자주 사용되는 조인이 3개 이상 | 대시보드 조회, 리포트 |
| 집계 쿼리가 실시간으로 필요 | 실시간 매출 합계 |
| 읽기/쓰기 비율이 10:1 이상 | 조회 위주 서비스 |
| 응답 시간 SLA가 매우 짧음 | 100ms 이내 응답 |
| 히스토리 데이터 대량 조회 | 분석 리포트, 통계 |
| 실시간 랭킹/카운트 필요 | 인기 상품, 좋아요 수 |
반정규화 의사결정 프로세스
반정규화는 무조건 적용하면 안 됩니다. 체계적인 의사결정 과정을 거쳐야 합니다.
반정규화 기법
1. 테이블 병합
1:1 관계나 잘 변하지 않는 관계의 테이블을 하나로 합칩니다.
테이블 병합 유형 정리
| 유형 | 설명 | 적합한 경우 |
|---|---|---|
| 1:1 병합 | 두 테이블을 하나로 | 항상 함께 조회 |
| 슈퍼/서브타입 병합 | 상속 관계 통합 | 서브타입이 적고 컬럼 유사 |
| 1:N 병합 | 부모에 자식 정보 포함 | N이 작고 고정적 |
2. 중복 컬럼 추가
조인을 줄이기 위해 자주 참조하는 컬럼을 다른 테이블에 복사합니다.
이 경우 users의 이름이 변경되면 orders의 user_name도 갱신해야 합니다 — 갱신 이상의 위험을 감수하는 것입니다.
방법 1: 트리거 (동기 갱신)
CREATE TRIGGER trg_user_name_sync
AFTER UPDATE OF name ON users
FOR EACH ROW
BEGIN
UPDATE orders SET user_name = :NEW.name
WHERE user_id = :NEW.id;
END;
장점: 즉시 반영, 정합성 보장
단점: 트리거 부하, 대량 UPDATE 위험
방법 2: 애플리케이션 코드에서 동기화
// 사용자 이름 변경 시
userRepo.updateName(userId, newName);
orderRepo.updateUserName(userId, newName);
장점: 명시적 제어
단점: 코드 누락 위험, 트랜잭션 관리 필요
방법 3: 배치 동기화 (비동기)
-- 매일 새벽 전체 동기화
UPDATE orders o SET user_name = (
SELECT u.name FROM users u WHERE u.id = o.user_id
) WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id = o.user_id AND u.name <> o.user_name
);
장점: 단순, 부하 집중 관리
단점: 동기화 지연3. 요약 테이블 (Summary Table)
집계 결과를 미리 계산하여 별도 테이블에 저장합니다. OLAP 성격의 쿼리에 매우 효과적입니다.
-- 1. 요약 테이블 생성
CREATE TABLE daily_sales_summary (
sale_date DATE PRIMARY KEY,
total_orders NUMBER,
total_amount NUMBER(15, 2),
avg_amount NUMBER(10, 2),
max_amount NUMBER(12, 2),
new_users NUMBER,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP
);
-- 2. MERGE로 일별 갱신 (Upsert)
MERGE INTO daily_sales_summary s
USING (
SELECT TRUNC(order_date) sale_date,
COUNT(*) total_orders,
SUM(total) total_amount,
AVG(total) avg_amount,
MAX(total) max_amount
FROM orders
WHERE TRUNC(order_date) = TRUNC(SYSDATE - 1)
GROUP BY TRUNC(order_date)
) src ON (s.sale_date = src.sale_date)
WHEN MATCHED THEN
UPDATE SET total_orders = src.total_orders,
total_amount = src.total_amount,
avg_amount = src.avg_amount,
max_amount = src.max_amount,
updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT VALUES (src.sale_date, src.total_orders,
src.total_amount, src.avg_amount,
src.max_amount, 0, SYSTIMESTAMP);4. 파생 컬럼 (Derived Column)
계산 결과를 컬럼으로 저장합니다. 매번 계산하는 비용을 줄입니다.
-- 정규화: total을 매번 서브쿼리로 계산
SELECT o.id,
(SELECT SUM(oi.price * oi.quantity)
FROM order_items oi WHERE oi.order_id = o.id) AS total
FROM orders o;
-- 반정규화: total을 orders에 컬럼으로 저장
ALTER TABLE orders ADD total NUMBER(12, 2);
-- 주문 생성/수정 시 total 계산하여 저장
UPDATE orders SET total = (
SELECT SUM(price * quantity)
FROM order_items WHERE order_id = orders.id
) WHERE id = :order_id;5. 테이블 분할 (반정규화 관점)
정규화의 역방향이 아닌, 성능을 위한 테이블 분리입니다.
Materialized View (구체화 뷰)
반정규화의 대안으로, DBMS가 요약 데이터를 자동 관리하는 기능입니다. 수동으로 요약 테이블을 관리하는 것보다 안전합니다.
-- 일별 매출 집계를 MV로 생성
CREATE MATERIALIZED VIEW mv_daily_sales
BUILD IMMEDIATE
REFRESH FAST ON COMMIT
ENABLE QUERY REWRITE
AS
SELECT TRUNC(order_date) AS sale_date,
COUNT(*) AS total_orders,
SUM(total) AS total_amount,
AVG(total) AS avg_amount
FROM orders
GROUP BY TRUNC(order_date);캐시와 반정규화
애플리케이션 레벨 캐시도 반정규화의 일종으로 볼 수 있습니다. DB 반정규화 전에 캐시를 먼저 검토해야 합니다.
반정규화와 NoSQL
NoSQL 데이터베이스는 설계 철학 자체가 반정규화에 기반합니다.
임베딩 (반정규화) 적합
* 함께 조회되는 데이터
* 변경 빈도 낮은 데이터
* 1:Few 관계 (예: 주소, 전화번호)
* 문서 크기 16MB 이내 (MongoDB 제한)
참조 (정규화) 적합
* 독립적으로 자주 조회/수정되는 데이터
* 1:Many (매우 많은) 관계
* Many:Many 관계
* 데이터 크기가 문서 제한 초과 가능정규화 vs 반정규화 판단 기준
| 기준 | 정규화 유리 | 반정규화 유리 |
|---|---|---|
| 데이터 변경 빈도 | 잦은 갱신 | 거의 변경 없음 |
| 조회 패턴 | 다양한 조건 조회 | 고정된 패턴 읽기 |
| 데이터 무결성 | 최우선 | 약간의 불일치 허용 |
| 응답 시간 | 여유 있음 | 매우 짧아야 함 |
| 시스템 유형 | OLTP | OLAP, 대시보드 |
| 데이터 크기 | 적음~보통 | 대용량 |
| 복잡도 관리 | 단순한 코드 | 동기화 코드 추가 감수 |
OLTP vs OLAP 관점 비교
정규화/반정규화 결정에서 시스템 유형은 가장 큰 영향을 미칩니다.
반정규화 부작용과 대응
반정규화는 성능을 얻는 대신 부작용을 수반합니다. 부작용을 인지하고 관리해야 합니다.
반정규화 종합 정리
| 기법 | 효과 | 부작용 | 정합성 관리 |
|---|---|---|---|
| 테이블 병합 | JOIN 제거 | NULL 증가 | 낮음 |
| 중복 컬럼 | 조인 불필요 | 갱신 이상 | 트리거/배치 |
| 요약 테이블 | 집계 즉시 | 공간, 갱신 비용 | 배치/MV |
| 파생 컬럼 | 계산 비용 제거 | 갱신 이상 | 트리거/코드 |
| 수직 분할 | I/O 감소 | 조인 필요 (역행) | 없음 |
| 수평 분할 | 스캔 범위 축소 | 쿼리 복잡 | 파티션 관리 |
정규화와 반정규화는 양자택일이 아닙니다. 기본적으로 3NF까지 정규화하고, 성능 병목이 측정으로 확인된 곳에만 반정규화를 선별적으로 적용하는 것이 바람직합니다.
다음 장에서는 쿼리 성능의 핵심인 인덱스를 다루겠습니다.