“Data modeling should start before you write code.” SQL을 작성하거나 ORM으로 객체를 정의하기 전에 비즈니스와 데이터 구조부터 이해해야 한다는 글을 읽었습니다. (저자는 1차 정규형조차 지키지 않은 사례도 지적했습니다.)

익숙한 주제였지만 글에 달린 참고자료 7개가 눈에 들어와 모두 읽어봤습니다. 킴볼(Kimball) 방법론부터 이력 관리, 조직 부채까지 이어지는 내용을 제 모델링 경험과 연결해 정리했습니다.

킴볼이 30년 전에 정해둔 두 가지

킴볼 방법론1은 4단계로 진행됩니다. 분석할 비즈니스 프로세스를 고르고 데이터의 집계 단위인 그레인(grain)을 선언한 뒤, 분석 기준이 되는 디멘션(dimension)과 측정값인 팩트(fact)를 찾습니다. 도구가 바뀌어도 여전히 설계의 핵심으로 남은 원칙은 이 중 두 가지입니다.

1. 그레인 선언

데이터 한 행(row)이 무엇을 나타내는지가 그레인입니다. 편의점 매출 테이블을 예로 들면 영수증 한 장인지, 상품 항목 하나인지, 매장별 하루 합계인지에 따라 답이 달라집니다. 영수증 내 상품 항목 단위로 저장하면 어떤 담배가 잘 팔리는지까지 조회할 수 있지만, 매장별 일별 합계만 남기면 상품별 매출은 조회할 수 없습니다. 합계에서 상품별 상세 내역을 복원할 수 없기 때문입니다. 그레인을 명확히 정하지 않으면 한 테이블에 영수증 단위 데이터와 매장별 하루 합계 데이터가 섞여 매출액이 중복 집계될 수 있습니다. 킴볼이 그레인 선언을 두 번째 단계에 둔 이유입니다. 디멘션과 팩트 역시 이 그레인을 기준으로 결정됩니다.

%%{init: {"look": "classic"}}%%
flowchart TD
    A["상품 항목 단위 · 3행<br/>한 행 = 영수증 내 상품 항목 하나"]
    A -->|영수증별 합산| B["영수증 단위 · 2행<br/>한 행 = 영수증 한 장"]
    B -->|매장별·일별 합산| C["일별 매출 단위 · 1행<br/>한 행 = 매장 × 날짜"]

예시 매출 합계는 모두 4,000원입니다. 합계만 저장하면 상품별 상세 내역을 복원할 수 없습니다.

2. 컨폼드 디멘션

컨폼드 디멘션(conformed dimension)2은 여러 팩트 테이블이 고객 같은 분석 기준을 일관되게 사용하도록 정의와 속성값을 맞춘 디멘션입니다. 매출, 상담, 배송 테이블에는 모두 고객이 있습니다. 세 시스템이 ID를 따로 매기고 등급 기준도 다르면 매출과 상담 데이터를 조인해도 고객 ID와 등급 기준이 달라 집계 결과가 맞지 않습니다. 공통 고객 테이블을 하나 두고 매출·상담·배송 테이블이 이를 참조하게 하는 것이 한 가지 구현 방법입니다. 어느 테이블에서 VIP라고 하든 같은 정의가 됩니다.

업무 개념의 정의를 여러 시스템에서 일관되게 사용한다는 점은 제가 온톨로지를 설명할 때 강조해온 부분이기도 합니다. 온톨로지는 고객이나 매출 같은 업무 개념의 정의와 개념 사이의 관계를 한곳에 정리해 두고 여러 시스템이 참조하게 하는 체계입니다. 여러 데이터를 한 화면에서 조회한다고 해서 고객이나 매출의 정의까지 같아지는 것은 아닙니다. 조회 창구를 합치는 일과 정의를 맞추는 일은 다릅니다. 컨폼드 디멘션과 온톨로지는 업무 개념의 정의를 일관되게 관리하려는 목적이 같습니다. 가전사 챗봇 프로젝트에서 여러 원천 데이터를 데이터 마트로 통합할 때도 마찬가지였습니다. 당시에는 그렇게 부르지 않았지만 돌이켜보면 컨폼드 디멘션을 구축하고 각 테이블의 그레인을 명확히 정의하는 작업이었습니다.

Kimball Group의 마지막 디자인 팁 은 2015년 글입니다. 스타 스키마를 dbt 모델로 구현하고 지표 정의를 시맨틱 레이어(semantic layer)3에서 관리해도 이 원칙은 적용됩니다. dbt는 데이터 변환 작업을 코드로 관리하는 도구이고, 시맨틱 레이어는 지표와 비즈니스 용어 정의를 중앙에서 모아 관리하는 계층입니다. 최신 도구를 도입하더라도 각 테이블의 그레인과 공통 디멘션을 정의하는 일은 결국 설계자의 몫입니다.

LLM이 SQL을 생성할 때도 그레인 정보가 필요합니다. 이 정보가 주어지지 않으면 이름이 같은 컬럼을 기준으로 그레인이 다른 두 팩트 테이블을 조인해 집계값을 부풀릴 수 있습니다. 자연어 질문을 SQL로 바꾸는 NL-to-SQL4이나 에이전트가 데이터를 조회한다면 정의를 통일하는 일이 더 중요해집니다. 온톨로지가 왜 필요한지 설득할 때 “AI 시대라서"보다 이 설명이 더 잘 통합니다.

실무에서 쓰는 구현 방식 네 가지

원칙은 그대로여도 구현은 회사마다 다릅니다. Seattle Data Guy가 정리한 실무 패턴 글 에 저자가 직접 본 방식 네 가지가 나옵니다. 저자는 킴볼 방법론을 쓴다고 해도 실제 모델에는 fact, dim이라는 이름만 남은 경우가 많다고 먼저 짚습니다.

1. SCD Type 2

SCD Type 25는 Slowly Changing Dimension Type 2의 약자로, 이력을 남기며 디멘션을 관리하는 방식입니다. 저자가 가장 자주 접한 방식입니다. 값이 바뀔 때마다 새 행을 넣고 그 값이 유효한 시작일과 종료일을 기록합니다. 직원이 어떤 직급에 며칠 있었는지처럼 기간 자체를 물을 때 필요합니다. 저자는 BETWEEN 절이 단순해지도록 아직 유효한 행의 종료일을 NULL로 두지 말고 9999-12-31처럼 먼 미래 날짜를 넣으라고 권합니다. Type 1은 값을 덮어쓰므로 변경 전 값이 남지 않습니다. Type 6 하이브리드는 일부 사례를 봤다는 정도로만 언급합니다.

2. 일별 스냅샷 파티션

일별 스냅샷 파티션6은 글에서 페이스북 사례로 소개하는 방식입니다. 디멘션의 변경 이력을 SCD로 관리하는 대신 매일 테이블 전체를 복사해 쌓아둡니다. 직원 테이블이라면 그날의 전체 직원 데이터가 날짜별 파티션에 저장됩니다. 조인할 때는 BETWEEN으로 시작일과 종료일을 비교하는 대신 ds 컬럼의 날짜로 바로 조인합니다. ds는 데이터를 기록한 기준 일자(datestamp)입니다. 직원이 페이지를 클릭한 날 어떤 직급이었는지 알고 싶다면 클릭 로그의 날짜와 ds를 맞추면 됩니다. 조회 쿼리는 단순해지지만 필요한 저장 공간은 늘어납니다.

%%{init: {"look": "classic"}}%%
flowchart TB
    subgraph SCD["SCD Type 2 · 변경 시 새 행 저장"]
        direction LR
        S1["일반<br/>시작 09-01<br/>종료 09-03"] -->|등급 변경| S2["VIP<br/>시작 09-03<br/>종료 NULL"]
    end
    subgraph SNAP["일별 스냅샷 · 매일 상태 저장"]
        direction LR
        D1["09-01<br/>C01 · 일반"] --- D2["09-02<br/>C01 · 일반"] --- D3["09-03<br/>C01 · VIP"]
    end
    SCD ~~~ SNAP

고객 C01이 09-03에 일반에서 VIP로 바뀐 예시입니다. SCD2는 시작일 포함·종료일 제외이며 NULL은 현재 유효함을 뜻합니다. 일별 스냅샷만으로 하루 안의 모든 상태 변화를 구분할 수는 없습니다.

3. date-list 집계

date-list 집계7는 Roblox 사례로 소개됩니다. 개별 방문 기록을 하나씩 저장하는 대신 사용자마다 방문한 날짜를 배열 하나에 모아 저장합니다. 이 사례에서는 하루에 스캔하는 데이터가 페타바이트에서 테라바이트로 줄었습니다. 다만 이 배열에는 개별 이벤트 내역이 남지 않으므로 재방문 여부나 월간 활성 사용자 수를 계산하는 데 적합합니다.

4. 팩트 정정 처리

팩트 정정 처리8는 이미 쌓아둔 데이터가 나중에 틀린 것으로 밝혀졌을 때 어떻게 바로잡을지 정하는 방식입니다. 기존 행을 UPDATE하는 대신 음수 보정 행을 넣거나 반품을 별도 테이블로 뺍니다. 저자는 의료 청구 데이터를 예로 듭니다. 심사 조정이 생기면 기존 청구 행을 대체할 수도 있고 음수 보정 행을 추가할 수도 있습니다. 정정 내역을 다른 테이블에 따로 기록하기도 합니다. 원천 시스템이 정정을 어떻게 기록하는지에 따라 처리 방식이 달라집니다. 원천 시스템의 정정 방식을 모델에 반영하지 않으면 쿼리가 정상 실행되더라도 잘못된 집계 결과가 나올 수 있습니다.

구현 방식을 고를 때는 저장 비용과 쿼리 복잡도를 함께 따져봅니다. 여기에 더해 조회 주체가 사람인지 LLM인지도 선택을 가르는 기준이 됩니다.

LLM 조회를 고려한 이력 관리 방식 선택

일 단위 상태 조회가 주된 목적이고 저장 비용을 감당할 수 있는 클라우드 환경이라면 일별 스냅샷을 검토할 만합니다. “이 날짜의 상태"가 그 날짜의 파티션에 그대로 있어서 프롬프트로 조회 방법을 설명하기 쉽습니다. SCD2는 유효 기간의 시작과 끝을 포함하는 기준을 설명해야 하며 종료일을 NULL로 저장한다면 그 처리도 필요합니다. 이때 OR end_date IS NULL9 같은 조건이 빠지면 현재 유효한 행을 놓칠 수 있습니다. 일별 스냅샷은 생성된 SQL에서 ds 날짜 조건을 확인하기 쉽습니다. 다만 질문에 맞는 기준 날짜인지, 조인 키와 그레인이 맞는지, 중복 집계가 없는지도 검증해야 합니다. 하루 안의 상태 변화까지 구분해야 한다면 일별 스냅샷만으로는 부족합니다.

저장 용량이 제한된 공공 온프레미스 환경에서는 디멘션 크기와 보관 기간을 함께 따져봐야 합니다. 행 수가 수천 단위이고 컬럼 수가 적은 조직 코드나 상병 코드 테이블은 보관 기간을 고려하더라도 매일 스냅샷을 찍는 부담이 상대적으로 적습니다. 반면 수천만 건에 달하는 수진자 테이블을 매일 복사하면 저장 용량이 빠르게 한계에 부딪힙니다. 디멘션의 크기와 변경 빈도, 보관 주기에 따라 서로 다른 이력 관리 방식을 조합하는 이유입니다.

가장 늦게 드러나지만 가장 갚기 어려운 부채

Joe Reis의 글 은 모델링 선택에 따른 기술·데이터·조직 부채를 다룹니다. 모델링에는 공짜 점심이 없다는 이야기입니다. 부채는 기술 부채, 데이터 부채, 조직 부채 세 가지로 나뉩니다.

설계를 느슨하게 하고 빠르게 진행하면 기술 부채와 데이터 부채가 쌓입니다. 이해관계자가 숫자를 의심하기 시작하면 데이터팀에 대한 신뢰도 흔들립니다. 팀마다 숫자가 다르다는 말, 한 번 나오면 끝입니다. 그 뒤로는 데이터팀이 무엇을 만들어도 의심부터 받습니다.

시간을 들여 엄밀하게 설계하면 기술 부채와 데이터 부채는 줄일 수 있습니다. 하지만 눈에 보이는 성과 없이 일정만 늘어지면 조직의 신뢰를 잃습니다. 어느 쪽이든 잃어버린 신뢰를 되찾아야 하는 상황이 생길 수 있습니다. Reis의 표현을 빌리면 모델이 없는 것도 모델이고 그냥 단지 형편없는 모델일 뿐입니다.

조직의 인내심을 펀치 패스(punch pass)에 빗댄 대목도 특이했습니다. 지름길을 택할 때마다 회수권에 구멍이 하나씩 뚫리고, 카드가 다 뚫리면 그동안 미뤄둔 문제를 한꺼번에 해결해야 합니다. 신뢰가 무너졌다는 사실은 기술이나 데이터의 문제보다 늦게 드러나지만, 신뢰를 되찾는 데 드는 시간과 노력은 더 큽니다.

프로젝트에서 회의 중 숫자 하나가 맞지 않았던 일이 이후 산출물 검토에도 영향을 미친 적이 있습니다. 시스템 장애가 아니라 집계 기준 차이로 생긴 오류였지만, 그 뒤로는 보고서의 모든 숫자가 재검증 대상이 되었습니다. Data Warehousing Essentials 에서도 품질 검증을 파이프라인을 만든 뒤 모니터링할 일로만 남겨두지 말고, 모델링 단계의 산출물에 포함하라고 강조합니다.

정리

1. 같은 질문을 같은 기준으로 해석한다

“VIP 고객의 지난달 매출"은 현재 VIP인 고객을 뜻하는지, 구매 당시 VIP였던 고객을 뜻하는지에 따라 결과가 달라집니다. 온톨로지 에는 고객·등급 같은 업무 개념과 관계를, 시맨틱 레이어에는 지표 계산식을, 메타데이터에는 테이블·컬럼과 그레인·조인 조건을 명시할 수 있습니다. LLM에게 조회를 맡기려면 이 정의들이 같은 고객 기준과 시점을 가리키는지부터 확인해야 합니다.

2. 저장하지 않을 데이터와 포기할 질문을 함께 정한다

일별 합계만 남기면 나중에 상품별 매출을 물어도 답할 수 없습니다. 저장 비용을 줄일 때는 보존할 상세 수준과 이력의 시간 단위뿐 아니라, 그 선택으로 답할 수 없게 되는 질문도 적어두는 편이 좋습니다. 그래야 이후의 요구사항 변경을 설계 오류와 구분해서 논의할 수 있습니다.

3. 정의가 바뀌면 검증 쿼리도 함께 고친다

두 팩트 테이블을 어떤 단위로 집계한 뒤 조인할지, 조인 후 금액이나 건수가 중복되지 않는지를 검증 쿼리로 남겨야 합니다. VIP 기준이 바뀌면 정의 문서와 함께 관련 지표·조회 규칙·검증 결과도 점검해야 합니다. 온톨로지를 운영할 때도 업무 기준의 변경이 실제 데이터와 쿼리에 반영됐는지까지 확인해야 합니다.

4. 먼저 제공할 지표부터 합의하고 검증한다

우선 제공할 지표의 그레인, 기준 시점, 정정 처리 규칙부터 현업과 합의하고 결과를 확인하는 편이 현실적입니다. 모든 요구가 확정되기를 기다리는 동안에도 일정은 흘러가므로, 지금 확정할 기준과 나중에 검토할 범위를 구분해야 합니다.

회의에서 숫자가 어긋났던 경험을 돌아보면 어느 집계 기준을 비교해야 하는지가 분명해야 설명도 가능합니다. 숫자를 의심하기 시작한 조직의 신뢰를 되찾는 데는 쿼리 하나를 고치는 것보다 더 많은 시간과 노력이 듭니다.

  1. 킴볼 방법론(Kimball methodology) 및 스타 스키마: 데이터베이스 구조를 팩트(측정값)와 디멘션(분석 기준)으로 나누어 설계하는 고전적 방법론입니다. ↩︎

  2. 컨폼드 디멘션(Conformed Dimension) & 온톨로지: 서로 다른 여러 시스템(매출, 상담, 배송) 간에 ‘고객’이나 ‘VIP’ 같은 개념의 정의와 ID 기준을 일관되게 맞추는 작업입니다. ↩︎

  3. dbt & 시맨틱 레이어(Semantic Layer): 데이터 변환 과정을 코드로 자동화하는 도구(dbt)와, 비즈니스 지표의 정의를 중앙에서 통합 관리하는 가상 계층입니다. ↩︎

  4. NL2SQL 및 그레인(Grain) 미스매치: AI가 자연어 질문을 SQL 쿼리로 바꿀 때, 집계 단위(그레인)가 다른 두 테이블을 구분하지 못하고 잘못 조인(JOIN)하여 중복 집계 오류를 내는 현상입니다. ↩︎

  5. SCD Type 2 (Slowly Changing Dimension): 데이터가 변경될 때 덮어쓰지 않고 시작일과 종료일(또는 9999-12-31)을 적어 이력을 유지하는 방식입니다. SQL의 BETWEEN 조건문 이해가 필요합니다. ↩︎

  6. 일별 스냅샷 파티션: 이력 관리 대신 매일 데이터 전체를 복사해 날짜 파티션(ds)별로 저장하는 구조로, 저장 공간 소모가 큽니다. ↩︎

  7. date-list 집계: 개별 접속 로그를 다 남기지 않고 사용자별 방문 날짜만 배열 형태의 하나의 컬럼에 모아 요약 저장하는 기술입니다. ↩︎

  8. 팩트 정정 처리 (음수 보정 행): 데이터 오류 발생 시 기존 행을 수정(UPDATE)하지 않고, -1을 곱한 보정 데이터를 추가해 합계를 맞추는 회계적 데이터 처리법입니다. ↩︎

  9. SQL 조건 누락 (OR end_date IS NULL): SCD2 구조에서 AI가 현재 진행 중인 상태의 유효 기간 조건을 빼먹어 잘못된 데이터를 조회하는 기술적 특성입니다. ↩︎