Skip to main content

왜 데이터 모델링을 알아야 하나요?

데이터베이스에 데이터를 저장할 때, “테이블을 어떻게 설계하느냐”에 따라 저장 효율, 쿼리 성능, 유지보수 편의성이 크게 달라집니다. 이 설계 과정을 데이터 모델링(Data Modeling) 이라고 합니다. 현업에서 20년 가까이 데이터를 다루면서 가장 많이 본 문제는, 모델링을 대충 하고 시작해서 6개월 후에 전체 파이프라인을 뜯어고치는 상황입니다. 모델링은 “나중에 하면 되지”가 통하지 않습니다. 데이터가 쌓이기 시작하면 구조를 바꾸는 비용은 기하급수적으로 늘어납니다. 데이터 모델링은 크게 두 가지 방향이 있습니다.
방향목적적합한 시스템
정규화 (Normalization)중복을 제거하고 무결성을 보장합니다OLTP (운영 시스템)
비정규화 (Denormalization)분석 성능을 극대화합니다OLAP (분석 시스템, 웨어하우스)

정규화 (Normalization)

개념

💡 정규화(Normalization) 란 데이터의 중복을 최소화 하기 위해 테이블을 분리하고, 관계를 통해 연결하는 설계 방법입니다. 데이터의 일관성과 무결성을 보장하는 것이 목표입니다.

비유: 고객 정보가 100군데에 흩어져 있다면?

실무에서 정규화가 왜 중요한지, 실제 장애 사례로 설명하겠습니다. 한 이커머스 회사에서 고객 주소를 주문 테이블에 직접 넣어두었습니다. 어느 날 고객이 주소를 변경했는데, 과거 주문 300건의 주소는 그대로였습니다. 배송팀에서 “이 주소가 맞나요?”라는 CS가 폭주했고, 결국 수동으로 데이터를 수정하는 데 이틀이 걸렸습니다. 정규화하지 않은 경우 (중복 가득):
주문번호고객명고객전화고객주소상품명가격
001김철수010-1234서울시 강남구노트북120만
002김철수010-1234서울시 강남구키보드8.9만
003이영희010-5678부산시 해운대구마우스6.5만
문제점: 김철수의 전화번호가 변경되면, 해당 고객의 모든 주문 행을 수정해야 합니다. 주문이 10만 건이면 10만 건을 업데이트해야 합니다. 하나라도 놓치면 데이터 불일치가 발생하고, 이런 불일치는 보통 3개월 후에야 발견됩니다. 정규화한 경우 (중복 제거): 고객 테이블:
customer_idnamephoneaddress
C1김철수010-1234서울시 강남구
C2이영희010-5678부산시 해운대구
주문 테이블:
order_idcustomer_idproductprice
001C1노트북120만
002C1키보드8.9만
003C2마우스6.5만
✅ 김철수의 전화번호를 고객 테이블에서 한 번만 수정하면, 모든 주문에서 올바른 번호가 조회됩니다.

정규화 단계

단계이름규칙실무에서의 의미
1NF제1정규형원자값만 저장”서울, 부산” 같은 쉼표 구분 값을 넣지 마세요. 나중에 WHERE city = '서울' 쿼리가 제대로 안 됩니다
2NF제2정규형부분 종속 제거복합 키를 쓸 때, 키의 일부에만 의존하는 컬럼을 분리합니다
3NF제3정규형이행 종속 제거”우편번호 → 도시” 같은 간접 종속을 별도 테이블로 뺍니다
💡 실무에서는 보통 3NF(제3정규형) 까지 적용하면 충분합니다. BCNF, 4NF, 5NF까지 가는 프로젝트는 20년 동안 손에 꼽을 정도로 드뭅니다. 과도한 정규화는 오히려 개발 생산성을 떨어뜨리고, JOIN이 너무 많아져서 쿼리 성능 문제를 일으킵니다.

정규화의 장단점

장점단점
데이터 중복이 없어 저장 공간 절약JOIN이 많아져 분석 쿼리가 느려질 수 있습니다
데이터 수정 시 한 곳만 변경하면 됩니다테이블이 많아져 신규 개발자가 구조 파악에 시간이 걸립니다
데이터 일관성이 보장됩니다대규모 분석 쿼리에서 7~8개 테이블을 JOIN하면 성능이 급격히 떨어집니다

비정규화 — 분석을 위한 설계

OLTP vs OLAP 설계의 차이

💡 OLTP(Online Transaction Processing)OLAP(Online Analytical Processing) 은 서로 다른 목적을 가진 시스템이며, 데이터 모델링 방식도 완전히 달라야 합니다. 가장 흔한 실수는 OLTP용으로 설계한 정규화 테이블을 그대로 분석에 쓰는 것입니다. 단순한 “이번 달 매출 합계” 쿼리도 5개 테이블을 JOIN해야 하고, 30초 이상 걸리게 됩니다.
비교OLTP (운영)OLAP (분석)
목적주문 처리, 결제, 회원가입 등 개별 트랜잭션매출 집계, 추이 분석, 리포트 등 대량 조회
쿼리 패턴소량의 행을 빠르게 읽기/쓰기 (한 번에 1~10건)수백만~수억 건을 집계 (SUM, AVG, COUNT)
설계 원칙정규화 (중복 제거)비정규화 (조인 최소화)
대표 도구MySQL, PostgreSQL, OracleDatabricks SQL, Snowflake, BigQuery

차원 모델링 (Dimensional Modeling)

분석 시스템(OLAP)을 위한 대표적인 설계 방법론이 차원 모델링 입니다. 1996년 Ralph Kimball이 체계화한 이 방법론은 30년이 지난 지금도 분석 모델링의 사실상 표준(de facto standard)입니다. 데이터를 팩트(Fact)디멘전(Dimension) 으로 나누어 설계합니다.
💡 팩트 테이블(Fact Table) 이란 비즈니스에서 발생하는 이벤트(사건) 와 그 측정값(숫자) 을 저장하는 테이블입니다. 주문, 클릭, 결제, 재고 이동 등의 이벤트가 해당합니다. 보통 테이블 중 가장 행 수가 많으며 — 대기업이라면 수십억 건이 기본입니다.
💡 디멘전 테이블(Dimension Table) 이란 팩트 이벤트의 맥락(누가, 언제, 어디서, 무엇을) 을 설명하는 참조 테이블입니다. 고객, 상품, 날짜, 지역 등이 해당합니다. 팩트 대비 행 수가 적지만(수천~수백만 건), 컬럼 수가 많고 텍스트 데이터가 풍부합니다.

Star 스키마 (Star Schema)

개념

💡 Star 스키마(별 모양 스키마) 는 중앙에 팩트 테이블을 두고, 주위에 디멘전 테이블을 직접 연결 하는 가장 기본적인 차원 모델링 패턴입니다. 다이어그램이 별 모양처럼 보여서 이런 이름이 붙었습니다.
결론부터 말씀드리면, 99%의 경우 Star 스키마가 정답 입니다. Snowflake 스키마를 선택해야 하는 경우는 뒤에서 설명하겠지만, 매우 드뭅니다.

핵심 디멘전 설계 — dim_date는 반드시 만드세요

Star 스키마를 설계할 때 가장 흔한 실수는 날짜 디멘전을 DATE 타입 컬럼 하나로 처리 하는 것입니다. “그냥 WHERE YEAR(order_date) = 2025 쓰면 되지 않나요?”라고 생각하실 수 있습니다. 하지만 실무에서는 이게 6개월 뒤 재앙이 됩니다.
-- 실무에서 필요한 dim_date 테이블 예시
CREATE TABLE dim_date (
    date_key      INT PRIMARY KEY,        -- 20250301 형식
    full_date     DATE NOT NULL,
    year          INT,
    quarter       STRING,                  -- 'Q1', 'Q2', ...
    month         INT,
    month_name    STRING,                  -- '3월', '4월', ...
    week_of_year  INT,
    day_of_week   STRING,                  -- '월요일', '화요일', ...
    day_of_week_num INT,                   -- 1(월) ~ 7(일)
    is_weekend    BOOLEAN,
    is_holiday    BOOLEAN,                 -- 공휴일 여부
    holiday_name  STRING,                  -- '설날', '추석', ...
    is_business_day BOOLEAN,              -- 영업일 여부
    fiscal_year   INT,                     -- 회계연도 (3월 결산이면 다를 수 있음)
    fiscal_quarter STRING,
    year_month    STRING                   -- '2025-03' 형식, 리포트용
);
왜 이게 중요한지 구체적으로 설명하겠습니다:
  1. 공휴일/영업일 계산: “영업일 기준 5일 이내 배송” 같은 비즈니스 룰을 쿼리할 때, WHERE d.is_business_day = true로 바로 필터링할 수 있습니다. 이걸 매번 함수로 계산하면 쿼리당 3~5초가 추가됩니다.
  2. 회계연도: 한국 기업 중 12월 결산이 아닌 곳이 많습니다 (삼성 등 대기업은 12월이지만, 많은 기업이 3월 결산). fiscal_yearfiscal_quarter를 미리 계산해 두면 재무 리포트가 훨씬 간단해집니다.
  3. 대시보드 성능: 수백 개의 대시보드에서 WHERE YEAR(order_date) = 2025 AND MONTH(order_date) = 3을 반복하면, 매번 함수 연산이 발생합니다. WHERE d.year = 2025 AND d.month = 3으로 조인하면 사전 계산된 값을 읽기만 하므로 훨씬 빠릅니다.
⚠️ dim_date 없이 시작한 프로젝트가 6개월 후 모든 대시보드를 리팩토링하는 경우를 수없이 봤습니다. dim_date 테이블은 보통 1030년 치를 미리 생성해 두며 (약 1만1.1만 행), 한 번 만들면 거의 변경할 일이 없습니다. 만드는 데 30분이면 충분하고, 안 만들면 나중에 30일을 고생합니다.

Degenerate Dimension (퇴화 디멘전)

모든 속성을 디멘전 테이블로 빼야 하는 것은 아닙니다. 주문번호, 송장번호, 영수증 번호 같은 값은 팩트 테이블의 각 행을 식별하는 데 사용되지만, 별도의 디멘전 테이블로 뺄 의미가 없습니다. 이런 값을 Degenerate Dimension(퇴화 디멘전) 이라고 합니다.
-- 주문번호(order_number)는 팩트 테이블에 그대로 둡니다
-- 별도의 dim_order를 만들 필요가 없습니다
CREATE TABLE fact_orders (
    order_key       BIGINT,
    order_number    STRING,     -- ← Degenerate Dimension
    invoice_number  STRING,     -- ← Degenerate Dimension
    customer_key    INT,
    product_key     INT,
    date_key        INT,
    quantity        INT,
    amount          DECIMAL(15,2)
);
처음 모델링하는 분들이 “모든 걸 디멘전으로 빼야 한다”고 오해해서, dim_order 같은 불필요한 테이블을 만드는 경우가 있습니다. 주문번호에 대한 추가 속성(상태, 유형 등)이 없다면, 팩트에 놔두는 것이 맞습니다.

Junk Dimension (정크 디멘전)

팩트 테이블에 is_returned, is_gift_wrapped, is_priority, payment_type 같은 플래그/코드 컬럼 이 5~6개 이상 쌓이기 시작하면, 이것들을 하나의 Junk Dimension 으로 묶는 것이 좋습니다.
-- Before: 팩트 테이블이 플래그로 비대해짐
-- fact_orders에 is_returned, is_gift, is_priority, payment_type, channel ...

-- After: Junk Dimension으로 조합을 미리 만들어 둡니다
CREATE TABLE dim_order_flags (
    flag_key        INT PRIMARY KEY,
    is_returned     BOOLEAN,
    is_gift         BOOLEAN,
    is_priority     BOOLEAN,
    payment_type    STRING,     -- 'card', 'cash', 'transfer'
    channel         STRING      -- 'web', 'app', 'store'
);

-- 팩트에는 flag_key 하나만 남깁니다
-- fact_orders.flag_key → dim_order_flags.flag_key
이렇게 하면 팩트 테이블의 컬럼 수가 줄어들고, “신용카드 + 웹 + 일반배송” 같은 조합별 분석도 깔끔하게 할 수 있습니다.

Star 스키마 — 전체 예시

유형테이블주요 컬럼
팩트 테이블fact_ordersorder_key, order_number, customer_key, product_key, date_key, store_key, flag_key, quantity, amount
디멘전 테이블dim_customercustomer_key, name, email, city, tier
dim_productproduct_key, name, category, brand, price
dim_datedate_key, full_date, month, quarter, year, is_business_day, fiscal_year
dim_storestore_key, name, address, region
dim_order_flagsflag_key, is_returned, is_gift, payment_type, channel

분석 쿼리 예시

-- "2025년 Q1, 영업일 기준, 서울 지역의 카테고리별 매출"
-- dim_date가 있으니 이렇게 깔끔하게 쿼리할 수 있습니다
SELECT
    p.category,
    d.fiscal_quarter,
    SUM(f.amount) AS total_revenue,
    COUNT(f.order_key) AS order_count,
    AVG(f.amount) AS avg_order_value
FROM fact_orders f
JOIN dim_product p ON f.product_key = p.product_key
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_store s ON f.store_key = s.store_key
WHERE d.fiscal_year = 2025
  AND d.fiscal_quarter = 'Q1'
  AND d.is_business_day = true    -- 영업일만!
  AND s.region = '서울'
GROUP BY p.category, d.fiscal_quarter
ORDER BY total_revenue DESC;

Slowly Changing Dimension (SCD) — 디멘전 변경 처리

현업에서 “고객이 서울에서 부산으로 이사했을 때, 과거 주문은 서울로 봐야 하나, 부산으로 봐야 하나?”라는 질문이 반드시 나옵니다. 이것을 Slowly Changing Dimension(SCD) 이라고 합니다.
SCD 유형동작 방식언제 쓰나요
Type 1새 값으로 덮어쓰기(이력 없음)오타 수정, 이력이 중요하지 않을 때
Type 2새 행 추가, 기존 행에 종료일 기록고객 등급 변경, 조직 개편 등 이력 추적이 필수일 때
Type 3이전 값과 현재 값을 별도 컬럼으로 저장직전 값만 알면 될 때 (거의 안 씀)

Type 2가 정말 필요한 경우 vs 오버엔지니어링

솔직히 말씀드리면, Type 2를 구현해야 하는 경우는 생각보다 적습니다. 많은 팀이 “혹시 필요할까 봐” Type 2를 모든 디멘전에 적용하는데, 이는 명백한 오버엔지니어링입니다. Type 2가 진짜 필요한 경우:
  • 고객 등급(VIP → Gold)이 바뀌었을 때, “VIP일 때의 매출”과 “Gold일 때의 매출”을 구분해서 봐야 하는 경우
  • 영업 조직이 개편되었을 때, “개편 전 A팀 소속일 때 실적”을 따로 봐야 하는 경우
  • 규정/감사 요건으로 “당시 시점의 정보”를 보존해야 하는 경우
Type 1로 충분한 경우 (=Type 2가 오버엔지니어링):
  • 고객 전화번호, 이메일 변경 — 이전 전화번호로 분석할 일이 없습니다
  • 상품명 오타 수정
  • 주소 변경인데 지역별 매출 분석이 불필요한 경우
-- Type 2 SCD 예시: 고객 등급 변경 이력
-- customer_key는 Surrogate Key (자동 생성), customer_id는 Natural Key (비즈니스 키)
| customer_key | customer_id | name   | tier   | effective_date | end_date   | is_current |
|-------------|-------------|--------|--------|----------------|------------|------------|
| 1001        | C1          | 김철수 | Silver | 2024-01-01     | 2025-02-28 | false      |
| 1002        | C1          | 김철수 | Gold   | 2025-03-01     | 9999-12-31 | true       |
⚠️ Type 2를 적용하면 디멘전 테이블의 행 수가 빠르게 증가하고, JOIN 조건에 날짜 범위가 추가되어 쿼리 복잡도가 올라갑니다. “이 디멘전에 Type 2가 정말 필요한가?”를 반드시 비즈니스 담당자와 확인하세요.

Snowflake 스키마 (Snowflake Schema)

개념

💡 Snowflake 스키마(눈꽃 모양 스키마) 는 Star 스키마의 디멘전 테이블을 더 세분화(정규화)하여, 디멘전이 하위 디멘전을 참조 하는 구조입니다.

언제 Snowflake를 쓰나요? (거의 안 씁니다)

솔직히 말씀드리면, 현대 데이터 플랫폼에서 Snowflake 스키마를 선택하는 경우는 매우 드뭅니다. Star 스키마의 디멘전 중복으로 인한 저장 비용 증가가 문제가 되던 시절(스토리지가 TB당 수천 달러이던 2000년대)에는 의미가 있었습니다. 하지만 지금은 S3 스토리지가 TB당 월 23달러입니다. Snowflake 스키마를 고려해야 하는 유일한 상황:
  • 디멘전 테이블 하나가 수천만 건 이상 이고, 그 안에 반복되는 서브 엔티티(예: 상품 → 카테고리 → 부서)의 텍스트 데이터가 매우 크면서, 그 서브 엔티티를 독립적으로 관리해야 할 때

Star vs Snowflake 비교

비교 항목Star 스키마Snowflake 스키마
디멘전 구조하나의 넓은 테이블여러 정규화된 테이블
JOIN 수적음 (팩트 ↔ 디멘전만)많음 (디멘전 ↔ 하위 디멘전도)
쿼리 성능빠름상대적으로 느림 (JOIN 증가)
쿼리 작성 난이도쉬움복잡 (BI 분석가가 싫어합니다)
저장 공간디멘전 중복 있음중복 최소화
실무 권장✅ 거의 모든 경우매우 특수한 경우에만

One Big Table (OBT) 패턴 — 레이크하우스 시대의 비정규화

왜 OBT가 떠오르고 있는가

최근 Databricks, Snowflake 같은 현대 플랫폼에서는 모든 디멘전을 팩트에 미리 합쳐 놓은 하나의 큰 테이블(One Big Table, OBT) 패턴이 급격히 늘고 있습니다. 이유는 간단합니다:
  1. 스토리지가 압도적으로 저렴해졌습니다. S3에 Parquet/Delta로 저장하면 TB당 월 23달러. 중복 데이터 걱정할 비용이 아닙니다.
  2. 컬럼 기반 압축이 뛰어납니다. 같은 값이 반복되는 디멘전 컬럼(예: city = ‘서울’이 100만 번)은 압축 시 거의 공간을 차지하지 않습니다.
  3. JOIN이 없으니 쿼리가 빠릅니다. Star 스키마에서 4개 테이블을 JOIN하면 30초 걸리던 쿼리가, OBT에서는 3초에 끝납니다.
  4. BI 사용자가 SQL을 몰라도 됩니다. 테이블 하나만 알면 모든 분석이 가능하므로, 셀프 서비스 분석이 훨씬 쉬워집니다.
-- Gold 계층: 비정규화된 OBT 생성
CREATE OR REPLACE TABLE gold.wide_orders AS
SELECT
    f.order_key,
    f.order_number,
    f.quantity,
    f.amount,
    -- 고객 디멘전
    c.name AS customer_name,
    c.city AS customer_city,
    c.tier AS customer_tier,
    -- 상품 디멘전
    p.name AS product_name,
    p.category AS product_category,
    p.brand AS product_brand,
    -- 날짜 디멘전
    d.full_date AS order_date,
    d.month,
    d.quarter,
    d.year,
    d.is_business_day,
    d.fiscal_year,
    -- 플래그 디멘전
    fl.is_returned,
    fl.payment_type,
    fl.channel
FROM fact_orders f
JOIN dim_customer c ON f.customer_key = c.customer_key
JOIN dim_product p ON f.product_key = p.product_key
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_order_flags fl ON f.flag_key = fl.flag_key;

Star 스키마 vs OBT — 언제 뭘 쓰나요

상황추천 패턴이유
데이터 엔지니어가 관리하는 Silver/Gold 계층Star 스키마유지보수성, 데이터 일관성이 중요
BI 대시보드의 소스 테이블OBT분석가가 JOIN 없이 바로 사용
ML Feature StoreOBT모델 학습에 필요한 피처를 한 테이블에
Ad-hoc 분석이 많은 팀OBT셀프 서비스 분석 효율성
💡 현업에서는 Star와 OBT를 함께 씁니다. Silver 계층에서 정규화된 엔티티 테이블을 관리하고, Gold 계층에서 용도별 OBT를 생성하는 패턴이 가장 일반적입니다. “Star냐 OBT냐”의 양자택일이 아니라, 계층별로 다른 패턴을 적용 하는 것이 정답입니다.

Databricks 레이크하우스에서의 모델링 실전 팁

Medallion 아키텍처와 모델링의 결합

계층모델링 방식설명
Bronze원본 그대로모델링 없이 원본을 보존합니다. 스키마가 바뀌어도 괜찮습니다
Silver3NF에 가까운 정규화정제된 엔티티별 테이블 (고객, 주문, 상품 등). 여기가 “Single Source of Truth”입니다
GoldStar 스키마 또는 OBT비즈니스 분석용 팩트+디멘전 또는 용도별 비정규화 테이블

파티셔닝? Liquid Clustering을 쓰세요

전통적인 데이터 웨어하우스에서는 날짜나 지역 기준으로 파티셔닝(Partitioning) 을 해야 쿼리 성능이 나왔습니다. Databricks에서도 예전에는 PARTITIONED BY (date) 같은 설정이 필수였습니다. 하지만 이제는 Liquid Clustering 을 사용하세요. 파티셔닝의 모든 장점을 가지면서, 파티션 키를 나중에 바꿀 수도 있고, 데이터 분포가 변해도 자동으로 최적화됩니다.
-- 전통적 파티셔닝 (이제 권장하지 않음)
-- CREATE TABLE fact_orders (...) PARTITIONED BY (order_date);

-- Liquid Clustering (현재 권장)
CREATE TABLE fact_orders (
    order_key BIGINT,
    order_number STRING,
    customer_key INT,
    product_key INT,
    date_key INT,
    quantity INT,
    amount DECIMAL(15,2)
)
CLUSTER BY (date_key, customer_key);

-- 클러스터링 키는 나중에 변경 가능!
ALTER TABLE fact_orders CLUSTER BY (date_key, product_key);
⚠️ 파티셔닝으로 시작했다가 “파티션 키를 바꿔야 하는데 테이블을 다시 만들어야 합니다”라는 상황에 처한 팀을 많이 봤습니다. Liquid Clustering은 이 문제를 근본적으로 해결합니다. 신규 테이블은 반드시 Liquid Clustering을 사용하세요.

Delta Lake의 모델링 영향

Databricks의 Delta Lake를 쓰면, 전통적 RDBMS와 다른 몇 가지 설계 고려사항이 있습니다:
전통 RDBMSDelta Lake on Databricks
인덱스 생성 필수인덱스 불필요 (Liquid Clustering + Data Skipping)
파티셔닝 필수Liquid Clustering 권장
정규화로 저장 비용 절약비정규화해도 컬럼 압축으로 비용 미미
SCD Type 2 직접 구현Delta Lake의 MERGE + Time Travel로 간소화
물리적 삭제DELETE 후 VACUUM으로 정리

정리

핵심 개념실무 핵심
정규화OLTP, Silver 계층에 적합. 3NF까지만 적용하세요
Star 스키마분석 모델링의 기본. 99%는 Star가 정답입니다
dim_date반드시 만드세요. 공휴일, 영업일, 회계연도를 포함해야 합니다
Degenerate Dimension주문번호 같은 식별자는 팩트에 남기세요
Junk Dimension플래그/코드 컬럼이 5개 이상이면 묶으세요
SCD Type 2비즈니스가 정말 요구할 때만 적용하세요. 오버엔지니어링 주의
OBTGold 계층, 대시보드 소스로 적극 활용하세요
Liquid ClusteringDatabricks에서는 파티셔닝 대신 이것을 쓰세요

참고 링크