지금까지의 과정을 하나의 요구사항에 처음부터 끝까지 적용합니다.
1. 요구사항
쇼핑몰의 출고 작업을 관리하는 시스템을 만든다.
- 주문 하나에 여러 주문항목이 있고, 주문항목마다 출고 품목(의류, 전자, 도서)이 붙는다.
- 출고 품목은 SKU 코드로 식별되며, 같은 SKU 코드의 상품이 여러 주문에 쓰인다. 상품 이름과 판매 금액은 바뀐다. 그래도 과거 주문은 당시 값으로 재현되어야 한다.
- 출고 처리는 계획일, 실적일, 담당 셀러를 관리한다. 검수에 불합격하면 재처리를 한다.
- 상품 유형별로 관리 속성이 다르다 (의류: 크기·재질 / 전자: 보장 기간·모델 번호 / 도서: ISBN).
- 셀러는 자기 회사의 출고 작업만 조회할 수 있다.
- 경영진은 주문·주문항목·유형별 처리율과 월별 투입 시수를 대시보드로 본다. 과거 월 보고서는 당시 기준으로 재현되어야 한다.
- 포장 차수는 일 6만 건, 5년 보관.
2. 요구사항 분석 (2강)
모호성 질문과 가정된 답변
| 질문 | 가정 답변 | 설계 영향 |
|---|---|---|
| 한 주문에 같은 상품이 여러 개 있을 수 있나? | 예, 주문항목에 수량이 있음 | 출고 단위 = 상품 × 주문 × 일련번호 |
| 출고 창고가 바뀔 수 있나? | 예, 재고 이전 시 | 출고 창고는 출고 단위의 비식별 속성 |
| 재처리 시 이전 처리 기록을 보존하나? | 예, 검수 이력 | 포장 차수 |
| 셀러가 중간에 바뀌나? | 드물지만 있음 | 차수별로 셀러 기록 |
| 과거 보고서 재현의 기준은? | 월말 마감 시점의 상태 | 주기적 스냅샷 |
업무 규칙
| 규칙 | 유형 | 구현 |
|---|---|---|
| 실적일은 계획일 이후 7일 이내 | 행 간 (다른 테이블 참조) | 트리거 또는 서비스 계층 |
| 같은 출고 단위의 차수는 유일 | 유일성 | PK (단, 파티션 키 포함 → 아래 한계 참고) |
| 진행 중인 차수는 출고 단위 하나에 하나 | 행 간 | 서비스 계층 행 락 (파티셔닝 제약) |
| 의류는 크기 필수 | 값 (서브타입) | 서브타입 NOT NULL |
| 셀러는 자기 데이터만 | 보안 | RLS |
볼륨
| 엔티티 | 규모 | 결정 |
|---|---|---|
| 포장 차수 | 일 6만 × 5년 ≈ 1억 1,000만 | 월 파티셔닝 |
| 상품 마스터 | 수십만 | 일반 테이블 |
3. 개념 모델 (3~4강)
[주문] ┼┼───┼< [주문항목]
[상품] ┼┼───○< [출고 단위] >○───┼┼ [주문]
│
>○───┼┼ [주문항목] (소속 주문항목)
>○───┼┼ [출고 창고] (현재 출고 창고, 가변)
[셀러] ┼┼───○< [포장 차수] >┼───┼┼ [출고 단위]
[포장 차수] ┼┼───○< [검수]
[상품] ── 슈퍼타입 {의류, 전자, 도서} (배타, 완전)- 상품: 주문과 무관한 마스터 정보 (여러 주문에서 공유) → 주문과 분리한 것이 핵심 판단
- 출고 단위: 특정 주문에 들어가는 개별 상품 (상품 × 주문 × 일련번호)
- 포장 차수: 약한 엔티티, 재처리 이력
주문 시점 스냅샷 — 마스터를 분리한 대가
상품을 주문에서 분리한 설계는 대가가 있습니다. 상품 이름과 판매 금액은 계속 바뀝니다. 출고 단위에 상품 마스터 참조만 있으면, 주문을 조회할 때마다 오늘의 상품 이름과 오늘의 판매 금액이 따라 들어갑니다. 과거 주문이 조용히 다시 쓰인다는 것입니다.
그래서 마스터 참조와 주문 시점 스냅샷을 동시에 둡니다. 상품의 현재 모습은 상품 마스터가 갖고, 주문이 성립하면 그때의 상품 이름과 판매 금액을 주문항목의 product_name_snapshot·unit_price_snapshot에 복제로 남깁니다. 모습은 10강 반정규화와 같습니다. 하지만 동기화 수단은 반대입니다. 10강의 복제 컬럼은 원본이 바뀌면 따라 갱신해 정합성을 맞춥니다. 주문 시점 스냅샷은 원본이 바뀌어도 갱신하지 않는 것이 정합성입니다. 복제 컬럼마다 갱신 주체를 함께 적습니다. 그 기록이 없으면 나중에 누가 좋은 의도로 동기화 배치를 붙여 과거 주문이 깨집니다.
18강의 선분 이력을 상품에 두면 과거 값을 다시 만들 수 있습니다. 스냅샷 없이도 재현은 됩니다. 다만 주문을 조회할 때마다 시점 조인을 해야 하고, 주문 조회는 이 시스템에서 가장 잦은 조회입니다. 그래서 보통 두 가지를 함께 씁니다. 상품 마스터의 변경은 이력으로 남깁니다. 주문 문서는 스냅샷으로 둡니다. 어느 쪽에도 비용이 있습니다. 선택한 근거를 함께 남깁니다. 여기까지가 설계입니다.
4. 논리·물리 설계 (6~16강, PostgreSQL 기준)
-- 입점 셀러 (코드 마스터, 자연키)
CREATE TABLE seller (
seller_code VARCHAR(10) PRIMARY KEY,
seller_name VARCHAR(100) NOT NULL
);
-- 출고 창고 (코드 마스터)
CREATE TABLE fulfillment_center (
fc_id INT PRIMARY KEY,
fc_name VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
order_no VARCHAR(10) PRIMARY KEY,
order_type VARCHAR(20) NOT NULL
);
CREATE TABLE order_item (
item_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no VARCHAR(10) NOT NULL REFERENCES orders,
item_seq INT NOT NULL, -- 주문 내 항목 순번
item_name VARCHAR(20) NOT NULL, -- 주문 한 줄에 표시되는 표기 — 상품명에 옵션·수량을 붙인 값. 상품명 단독 값은 아래 스냅샷이 갖는다
product_name_snapshot VARCHAR(200) NOT NULL, -- 주문 시점 스냅샷 (원본이 바뀌어도 갱신하지 않는다)
unit_price_snapshot NUMERIC(12,2) NOT NULL, -- 주문 시점 스냅샷 (원본이 바뀌어도 갱신하지 않는다)
UNIQUE (order_no, item_seq),
UNIQUE (item_id, order_no) -- 복합 FK 대상
);
-- 슈퍼타입 + 서브타입 (11강 전략 C)
CREATE TABLE product (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku_code VARCHAR(30) NOT NULL UNIQUE, -- 자연키 보호
product_type CHAR(2) NOT NULL CHECK (product_type IN ('AP','EL','BK')), -- 유형코드(2자). 19·20강의 product_type은 유형명이다
description VARCHAR(200),
UNIQUE (product_id, product_type)
);
CREATE TABLE apparel (
product_id BIGINT PRIMARY KEY,
product_type CHAR(2) NOT NULL DEFAULT 'AP' CHECK (product_type = 'AP'),
size_label VARCHAR(10) NOT NULL,
material VARCHAR(20) NOT NULL,
FOREIGN KEY (product_id, product_type) REFERENCES product (product_id, product_type)
);
CREATE TABLE electronics (
product_id BIGINT PRIMARY KEY,
product_type CHAR(2) NOT NULL DEFAULT 'EL' CHECK (product_type = 'EL'),
warranty_months SMALLINT NOT NULL CHECK (warranty_months > 0),
model_no VARCHAR(30) NOT NULL,
FOREIGN KEY (product_id, product_type) REFERENCES product (product_id, product_type)
);
CREATE TABLE book (
product_id BIGINT PRIMARY KEY,
product_type CHAR(2) NOT NULL DEFAULT 'BK' CHECK (product_type = 'BK'),
isbn VARCHAR(13) NOT NULL,
author VARCHAR(100) NOT NULL,
FOREIGN KEY (product_id, product_type) REFERENCES product (product_id, product_type)
);
-- 출고 단위
CREATE TABLE ship_unit (
unit_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES product,
order_no VARCHAR(10) NOT NULL,
serial_no SMALLINT NOT NULL,
item_id BIGINT NOT NULL,
fc_id INT NOT NULL REFERENCES fulfillment_center, -- 현재 출고 창고 (가변)
plan_date DATE NOT NULL,
UNIQUE (product_id, order_no, serial_no), -- 자연키
FOREIGN KEY (item_id, order_no) REFERENCES order_item (item_id, order_no) -- 주문항목이 같은 주문 소속임을 보장
);
CREATE INDEX ix_unit_item ON ship_unit (item_id);
CREATE INDEX ix_unit_order ON ship_unit (order_no);
-- 포장 차수 (약한 엔티티, 월 파티셔닝)
CREATE TABLE pack_round (
unit_id BIGINT NOT NULL,
round_no SMALLINT NOT NULL,
seller_code VARCHAR(10) NOT NULL REFERENCES seller,
status VARCHAR(10) NOT NULL
CHECK (status IN ('ASSIGNED','PACKED','PASSED','FAILED')),
packed_on DATE,
man_hours NUMERIC(6,2) CHECK (man_hours >= 0),
work_month DATE NOT NULL, -- 파티션 키 (배정 월 1일)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (unit_id, round_no, work_month),
CHECK (EXTRACT(DAY FROM work_month) = 1),
CHECK (status = 'ASSIGNED' OR packed_on IS NOT NULL)
) PARTITION BY RANGE (work_month);
CREATE INDEX ix_round_seller ON pack_round (seller_code, work_month);
CREATE INDEX ix_round_unit ON pack_round (unit_id);설계 판단 기록
| 판단 | 근거 | 관련 강의 |
|---|---|---|
| 상품 마스터와 출고 단위 분리 | 여러 주문에서 공유 → 상품 속성이 여러 주문에 반복되는 갱신 이상 방지 | 7, 8강 |
| 슈퍼+서브타입 | 출고 단위가 상품 마스터를 참조해야 하므로 서브타입별 테이블 불가 | 11강 |
(item_id, order_no) 복합 FK | 다른 주문의 주문항목을 지정하는 오류를 DB가 차단 | 10강 |
| 포장 차수 PK에 work_month | 파티셔닝 제약, 월 단위 보관 관리 | 16강 |
| 주문 시점 스냅샷 컬럼 | 상품 이름·판매 금액이 바뀌어도 과거 주문은 당시 값으로 재현 | 10, 18강 |
| 상태 단일 컬럼 | 불리언 조합의 모순 방지 | 12강 |
| 실적일 범위 규칙 | 계획일은 다른 테이블 → CHECK 불가, 앱 또는 트리거로 검증 | 13강 |
마지막 행처럼 DB 제약으로 표현할 수 없는 규칙을 명시적으로 기록하는 것도 설계의 일부입니다. 이 규칙은 트리거로 구현하거나, 출고 단위에 계획일이 바뀔 때의 처리까지 포함해 앱 서비스 계층에서 검증합니다.
"차수 유일"과 "진행 중인 차수는 출고 단위 하나에 하나" 규칙은, 파티션 테이블의 PK·UNIQUE가 파티션 키(work_month)를 포함해야 하므로 월이 다른 행끼리는 DB 제약만으로 막을 수 없습니다. 이 한계는 파티셔닝과 무결성의 트레이드오프이며, 서비스 계층의 행 락(SELECT ... FOR UPDATE on ship_unit)으로 보완합니다.
5. 보안 (21강)
ALTER TABLE pack_round ENABLE ROW LEVEL SECURITY;
CREATE POLICY seller_own_rows ON pack_round
FOR SELECT TO seller_role
USING (seller_code = current_setting('app.seller_code'));사내 사용자 역할에는 별도 정책(USING (true))을 부여합니다.
6. 분석 모델 (20강)
버스 매트릭스
| 업무 \ 차원 | 월 | 주문 | 주문항목 | 상품 유형 | 셀러 |
|---|---|---|---|---|---|
| 포장 실적 (트랜잭션) | O | O | O | O | O |
| 월말 포장 현황 (주기적 스냅샷) | O | O | O | O |
CREATE TABLE fact_pack_monthly_snapshot (
month_key INT NOT NULL, -- 202609
item_sk BIGINT NOT NULL, -- SCD Type 2 대리키
product_type CHAR(2) NOT NULL,
planned_cnt INT NOT NULL, -- 분모
packed_cnt INT NOT NULL, -- 분자
passed_cnt INT NOT NULL,
man_hours NUMERIC(12,2) NOT NULL,
closed_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (month_key, item_sk, product_type)
);- 처리율은 저장하지 않고
packed_cnt / planned_cnt로 계산 (비가산 측정값) - 월말 마감 배치가 스냅샷을 삽입만 하므로, 과거 월 보고서가 그대로 재현됨 (18강 요구)
man_hours는 가산이지만, 개수 컬럼들은 월 차원으로 합산하면 안 되는 준가산 값
7. 진화 시나리오 (22강)
요구 변경: "의류 상품에 색상 사양을 추가하고, 향후 유형별 속성이 자주 늘어날 예정"
| 단계 | 작업 |
|---|---|
| 1 | product에 extra_spec JSONB NOT NULL DEFAULT '{}' 추가 (즉시 적용) |
| 2 | 새 속성은 JSON에 저장 (19강 하이브리드) |
| 3 | 3개월 후 color가 대시보드 필터로 쓰이기 시작 → 컬럼 승격 결정 |
| 4 | apparel.color 추가 → 앱 이중 쓰기 → Backfill (JSON에서 추출) |
| 5 | 읽기 전환 → 검증 → JSON 키 쓰기 중단 → JSON에서 키 제거 |
8. 리뷰 (24강)
| 항목 | 결과 |
|---|---|
| 대리키 테이블의 자연키 UNIQUE | 모두 있음 |
| FK 인덱스 | ship_unit.product_id는 UNIQUE (product_id, order_no, serial_no)의 선두 컬럼이라 커버됨, pack_round.seller_code는 복합 인덱스 선두로 커버됨. ship_unit.fc_id만 전용 인덱스 없음 → 중간 |
| 금액·시수 타입 | NUMERIC |
| DB로 표현 못한 규칙 | 3건 (실적일 범위, 월 간 차수 유일, 진행 차수 단일), 기록 및 서비스 계층 구현 확인 |
| 파티션 선 생성 배치 | 운영 계획에 포함 필요 → 높음 |
| 안티패턴 역추적 | 해당 없음 |
25강 정리
하나의 요구사항이 25개 강의의 판단을 모두 거쳐 스키마가 됩니다.
- 요구사항의 모호성 질문이 엔티티 분리(상품 마스터 vs 출고 단위)를 결정했다.
- 슈퍼/서브타입과 복합 FK로 업무 규칙을 구조로 강제했다.
- 파티셔닝은 보관 관리를 얻는 대신 일부 유일성 보장을 포기하게 했고, 그 한계를 기록했다.
- 분석 모델은 분자·분모 스냅샷으로 과거 재현 요구를 충족했다.
- 변화 요구는 JSON 하이브리드와 Expand–Contract로 흡수했다.
마치며
강의 전체 요약
| Part | 핵심 질문 | 핵심 도구 |
|---|---|---|
| 1. 기초 | 업무를 어떻게 정확히 이해하고 표현하나? | 요구사항 분석, ER 모델, 키 |
| 2. 논리 설계 | 사실을 어떻게 중복 없이 배치하나? | 변환 규칙, 함수 종속성, 정규화 |
| 3. 물리 설계 | DBMS 위에서 어떻게 효율적으로 저장하나? | 타입, 제약, 키 생성, 인덱스, 파티션 |
| 4. 심화 모델링 | 계층·시간·유연성·분석·테넌트를 어떻게 다루나? | 클로저 테이블, 선분 이력, JSON, 차원 모델, RLS |
| 5. 시니어 레벨 | 운영 중인 설계를 어떻게 바꾸고 나누고 검증하나? | Expand–Contract, 바운디드 컨텍스트, 리뷰, ADR |
설계자가 기억할 원칙
- 업무 규칙이 먼저, 테이블은 나중이다. FD도, 제약도, 인덱스도 업무 규칙과 쿼리에서 나온다.
- "같은 일이 두 번 일어날 수 있는가?" 이 질문 하나가 키 설계를 바꾼다.
- DB가 보장할 수 있는 것은 DB에 맡긴다. 앱 검증은 우회된다.
- 중복은 동기화 수단과 함께 설계한다. 가능하면 DB가 보장하는 수단을 쓴다.
- 나누기는 쉽고 합치기는 어렵다. 스키마도, 테이블도, DB도 필요가 확인될 때 나눈다.
- 표현하지 못한 규칙은 기록한다. 설계의 한계를 아는 것도 설계다.
- 스키마는 변한다. 모든 변경은 되돌릴 수 있는 단계로 나눈다.
참고 자료
- Martin Kleppmann, 데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications)
- Alex Petrov, Database Internals
- Bill Karwin, SQL AntiPatterns
- Ralph Kimball, Margy Ross, The Data Warehouse Toolkit
- Richard T. Snodgrass, Developing Time-Oriented Database Applications in SQL
- CMU 15-445/645 Database Systems
- 백은빈·이승현, Real MySQL 8.0
- 각 DBMS 공식 문서 (PostgreSQL, MySQL, Oracle, SQL Server)
시리즈를 마치며 — 25강 전체 목록
| 강 | 주제 |
|---|---|
| 1강 | 설계란 무엇인가 |
| 2강 | 요구사항 분석 |
| 3강 | 개념 모델링 (ER 모델) |
| 4강 | ERD 표기법 |
| 5강 | 키의 종류 |
| 6강 | ER → 관계형 변환 |
| 7강 | 이상현상과 함수 종속성 |
| 8강 | 정규화 1NF ~ BCNF |
| 9강 | 4NF, 5NF |
| 10강 | 반정규화 |
| 11강 | 식별/비식별 관계, 슈퍼타입/서브타입 |
| 12강 | 데이터 타입 설계 |
| 13강 | 제약조건 설계 |
| 14강 | 키 생성 전략 |
| 15강 | 인덱스 설계 |
| 16강 | 파티셔닝 설계 |
| 17강 | 계층 구조 모델링 |
| 18강 | 이력·시간 모델링 |
| 19강 | 유연한 구조: 다형 관계, EAV, JSON |
| 20강 | 차원 모델링 |
| 21강 | 멀티테넌시 설계 |
| 22강 | 스키마 진화 |
| 23강 | 도메인 경계와 DB 분리 |
| 24강 | 설계 리뷰 방법론 |
| 25강 | 종합 실습 |