icon

안동민 개발노트

8장 : 정규화

반정규화

정규화가 데이터 무결성을 위한 분해라면, 반정규화(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 성격의 쿼리에 매우 효과적입니다.

요약 테이블 생성과 갱신 (Oracle)
-- 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가 요약 데이터를 자동 관리하는 기능입니다. 수동으로 요약 테이블을 관리하는 것보다 안전합니다.

Oracle Materialized View
-- 일별 매출 집계를 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 데이터베이스는 설계 철학 자체가 반정규화에 기반합니다.

NoSQL 임베딩 vs 참조 판단
임베딩 (반정규화) 적합
  * 함께 조회되는 데이터
  * 변경 빈도 낮은 데이터
  * 1:Few 관계 (예: 주소, 전화번호)
  * 문서 크기 16MB 이내 (MongoDB 제한)

참조 (정규화) 적합
  * 독립적으로 자주 조회/수정되는 데이터
  * 1:Many (매우 많은) 관계
  * Many:Many 관계
  * 데이터 크기가 문서 제한 초과 가능

정규화 vs 반정규화 판단 기준

기준정규화 유리반정규화 유리
데이터 변경 빈도잦은 갱신거의 변경 없음
조회 패턴다양한 조건 조회고정된 패턴 읽기
데이터 무결성최우선약간의 불일치 허용
응답 시간여유 있음매우 짧아야 함
시스템 유형OLTPOLAP, 대시보드
데이터 크기적음~보통대용량
복잡도 관리단순한 코드동기화 코드 추가 감수

OLTP vs OLAP 관점 비교

정규화/반정규화 결정에서 시스템 유형은 가장 큰 영향을 미칩니다.


반정규화 부작용과 대응

반정규화는 성능을 얻는 대신 부작용을 수반합니다. 부작용을 인지하고 관리해야 합니다.


반정규화 종합 정리

기법효과부작용정합성 관리
테이블 병합JOIN 제거NULL 증가낮음
중복 컬럼조인 불필요갱신 이상트리거/배치
요약 테이블집계 즉시공간, 갱신 비용배치/MV
파생 컬럼계산 비용 제거갱신 이상트리거/코드
수직 분할I/O 감소조인 필요 (역행)없음
수평 분할스캔 범위 축소쿼리 복잡파티션 관리

정규화와 반정규화는 양자택일이 아닙니다. 기본적으로 3NF까지 정규화하고, 성능 병목이 측정으로 확인된 곳에만 반정규화를 선별적으로 적용하는 것이 바람직합니다.

다음 장에서는 쿼리 성능의 핵심인 인덱스를 다루겠습니다.