infra
Platform

모듈 맵

[Database] 요구사항 분석부터 정규화 및 ERD 작성 전략

0 / 37 완료

펼치기
0 / 37 완료0%

Database · 02 / 37

[Database] 요구사항 분석부터 정규화 및 ERD 작성 전략

엔티티, 속성, 관계를 파악하고 ERD로 데이터 구조를 시각화하는 방법을 익힙니다

🚨INCIDENT ALERT
HIGH

기능 구현을 서두르다 테이블을 먼저 만들면 나중에 관계가 꼬이고 중복 데이터가 쌓입니다. ERD는 그림을 예쁘게 그리는 도구가 아니라, 요구사항을 데이터 구조로 검증하는 안전장치입니다. 초기에 관계를 명확히 잡아야 API와 쿼리도 단순해집니다.

이번 챕터에서 배울 것

엔티티-관계 모델의 기본 개념을 익히고, 실제 쇼핑몰 도메인을 ERD로 설계하는 과정을 통해 데이터 모델링 역량을 키웁니다.

  • 1엔티티, 속성, 관계의 개념과 정의
  • 2까마귀발 표기법(Crow's Foot Notation)
  • 31:1, 1:N, N:M 관계 유형
  • 4쇼핑몰 ERD 실전 설계
  • 5N:M 중간 테이블 구현

데이터 모델링과 ERD — 테이블 설계의 시작점

데이터베이스 설계의 첫 번째 단계는 코드나 SQL이 아닙니다. 비즈니스 도메인을 이해하고, 어떤 데이터가 존재하며, 그 데이터들이 서로 어떤 관계를 맺는지를 시각화하는 것입니다. **ERD(Entity-Relationship Diagram)**는 바로 그 작업을 위한 표준 도구입니다.


💡개념

ERD 핵심 3요소 — 엔티티, 속성, 관계

새 기능 개발 전에 "ERD 먼저 그려봐"라는 말을 듣습니다. 네모와 선으로 이루어진 다이어그램인데, 어디서부터 시작해야 할지 모릅니다. 엔티티, 속성, 관계 — 이 세 가지를 정확히 이해해야 ERD가 코드보다 먼저 데이터 구조를 논의하는 팀 공용 언어가 됩니다.

ERD 핵심 3요소 — 엔티티, 속성, 관계

엔티티 (Entity)

엔티티는 비즈니스 도메인에서 독립적으로 식별 가능하고 데이터를 저장할 가치가 있는 객체나 개념입니다. ERD에서는 직사각형으로 표현되며, 데이터베이스에서 테이블이 됩니다.

엔티티를 식별할 때는 세 가지 질문을 던져봅니다. 이 개체에 대해 여러 데이터를 저장해야 하는가? 다른 개체와 구별되는 고유한 식별자가 있는가? 이 개체에 대한 쿼리가 자주 발생하는가? 쇼핑몰 도메인이라면 User, Product, Category, Order, OrderItem, Cart, Review가 엔티티 후보입니다.

강한 엔티티 vs 약한 엔티티:

모든 엔티티가 독립적으로 존재하는 것은 아닙니다. 부모 엔티티 없이는 의미가 없는 종속 엔티티를 약한 엔티티라고 부릅니다.

구분정의예시
강한 엔티티자체 PK로 독립 존재 가능User, Product, Category
약한 엔티티부모 엔티티에 종속, 부모 없이 의미 없음OrderItem (Order 없이는 의미 없음)

속성 (Attribute)

속성은 엔티티가 가지는 개별 데이터 항목입니다. ERD에서 타원으로 표현되며, DB에서 컬럼이 됩니다.

속성 종류설명예시
단순 속성더 이상 분해 불가age, price
복합 속성더 작은 단위로 분해 가능addresscity + street + zipcode
다값 속성여러 값을 가질 수 있음phone_numbers → 별도 테이블로 분리
유도 속성다른 속성으로부터 계산age (birthdate로부터 계산)
기본키 속성엔티티를 고유하게 식별user_id, product_id

관계 (Relationship)와 카디널리티

관계는 두 엔티티 사이의 연관성을 나타냅니다. **카디널리티(Cardinality)**는 관계에 참여하는 인스턴스의 수입니다.

까마귀발 표기법 (Crow's Foot Notation)

까마귀발 표기법은 선 끝 기호로 최솟값(왼쪽 기호)과 최댓값(오른쪽 기호)을 동시에 표현합니다. 원(○)은 "0개 가능"을, 수직선(|)은 "정확히 1개"를, 까마귀발(<)은 "여러 개"를 나타냅니다.

기호 의미:
  ─────|    정확히 1  (Exactly One)       — 반드시 1개 존재
  ────○|    0 또는 1  (Zero or One)       — 있거나 없거나
  ─────<    1 이상    (One or Many)       — 최소 1개, 여러 개 가능
  ────○<    0 이상    (Zero or Many)      — 없거나 여러 개 가능

1:N 관계 — User와 Order:
  User ─────|────○< Order
  (한 사용자)   (0개 이상의 주문)
  → 사용자는 반드시 1명, 주문은 0개 이상

N:M 관계 — Order와 Product:
  Order ○<────|────○< Product
  (여러 주문)       (여러 상품)
  → 한 주문에 여러 상품, 한 상품이 여러 주문에 포함

세 가지 관계 유형

1:1 관계 — 각 인스턴스가 정확히 하나의 상대 인스턴스와 연결됩니다. 구현 시 자식 테이블의 FK 컬럼에 UNIQUE 제약을 추가합니다.

User ─────|─────| UserProfile
한 사용자는 프로필을 정확히 하나만 가짐
구현: UserProfile.user_id UNIQUE FK

1:N 관계 — 하나의 인스턴스가 여러 상대 인스턴스와 연결됩니다. 가장 흔한 관계 유형입니다. 구현 시 N쪽 테이블에 FK 컬럼을 추가합니다.

Category ─────|────○< Product
한 카테고리에 여러 상품이 속함
구현: Product.category_id FK

N:M 관계 — 양쪽 모두 여러 인스턴스와 연결됩니다. RDBMS에서는 직접 표현할 수 없으므로 중간 테이블로 분해합니다.

Order ○<────────────○< Product
한 주문에 여러 상품, 한 상품이 여러 주문에 포함
구현: OrderItem 중간 테이블 필요
💡개념

실전 ERD 설계 — 쇼핑몰 데이터 모델링

ERD 표기법은 이해했는데 실제로 어떻게 그리는지 막막합니다. 상품, 주문, 사용자가 있는데 이들이 어떤 관계인지, N:M 관계를 테이블로 어떻게 표현하는지 모릅니다. 쇼핑몰 도메인은 JOIN, N:M, 중간 테이블을 모두 포함하는 실무 대표 케이스로, 이 설계를 따라가면 실제 프로젝트 ERD 작성이 가능해집니다.

실전 ERD 설계 — 쇼핑몰 데이터 모델링

쇼핑몰 도메인 분석

SQL을 작성하기 전에 비즈니스 요구사항을 먼저 문장으로 정리합니다. 요구사항에서 명사는 엔티티 후보, 동사는 관계 후보가 됩니다.

  • 사용자는 회원가입 후 로그인할 수 있다
  • 상품은 카테고리에 속한다 (카테고리는 계층 구조)
  • 사용자는 여러 상품을 장바구니에 담을 수 있다
  • 사용자는 주문을 생성하고, 주문에는 여러 상품이 포함된다
  • 사용자는 구매한 상품에 리뷰를 작성할 수 있다
  • 상품은 여러 이미지를 가질 수 있다

쇼핑몰 ERD (까마귀발 ASCII 다이어그램)

아래 다이어그램에서 화살표 방향은 FK가 위치하는 쪽(N쪽)을 가리킵니다. categoriesparent_id는 같은 테이블을 참조하는 자기참조 관계로 계층 구조를 표현합니다.

┌─────────────────┐       ┌─────────────────────┐
│    categories   │       │       products       │
├─────────────────┤       ├─────────────────────┤
│ PK id           │──┐    │ PK id               │
│    name         │  └──> │ FK category_id      │
│ FK parent_id ───┘       │    name             │
│    slug         │       │    description      │
│    created_at   │       │    price            │
└─────────────────┘       │    stock            │
                          │    created_at       │
                          └──────────┬──────────┘
                                     │
        ┌────────────────────────────┼────────────────────────┐
        │                           │                        │
        ▼                           ▼                        ▼
┌──────────────┐        ┌─────────────────┐       ┌──────────────────┐
│ product_imgs │        │   order_items   │       │    cart_items    │
├──────────────┤        ├─────────────────┤       ├──────────────────┤
│ PK id        │        │ PK id           │       │ PK id            │
│ FK product_id│        │ FK order_id     │       │ FK cart_id       │
│    url       │        │ FK product_id   │       │ FK product_id    │
│    sort_order│        │    quantity     │       │    quantity      │
└──────────────┘        │    unit_price   │       │    added_at      │
                        └────────┬────────┘       └────────┬─────────┘
                                 │                         │
                                 ▼                         ▼
┌─────────────────┐    ┌─────────────────┐      ┌─────────────────┐
│      users      │    │     orders      │      │      carts      │
├─────────────────┤    ├─────────────────┤      ├─────────────────┤
│ PK id           │───>│ PK id           │      │ PK id           │
│    email (UQ)   │    │ FK user_id      │      │ FK user_id (UQ) │
│    password     │    │    status       │      │    created_at   │
│    name         │    │    total        │      └─────────────────┘
│    created_at   │    │    created_at   │
└────────┬────────┘    └─────────────────┘
         │
         ▼
┌─────────────────┐
│     reviews     │
├─────────────────┤
│ PK id           │
│ FK user_id      │
│ FK product_id   │
│    rating (1-5) │
│    content      │
│    created_at   │
└─────────────────┘

SQL 구현 — 핵심 테이블 생성

ERD를 확정한 뒤 SQL로 변환합니다. 테이블 생성 순서는 참조 방향에 따라 결정됩니다. 참조 대상(부모 테이블)을 먼저 생성해야 FK 선언이 가능합니다. 여기서는 categories → products → users → orders → order_items 순서입니다.

order_itemsunit_priceproducts.price를 그대로 참조하는 것이 아니라 주문 시점의 가격을 별도로 저장합니다. 이후 상품 가격이 변경되어도 주문 당시 금액이 보존되어야 하기 때문입니다.

SQL
CREATE TABLE categories (
    id         SERIAL PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    slug       VARCHAR(100) UNIQUE NOT NULL,
    parent_id  INT REFERENCES categories(id) ON DELETE SET NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE products (
    id          SERIAL PRIMARY KEY,
    category_id INT NOT NULL REFERENCES categories(id),
    name        VARCHAR(255) NOT NULL,
    description TEXT,
    price       DECIMAL(12, 2) NOT NULL CHECK (price >= 0),
    stock       INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
    created_at  TIMESTAMP DEFAULT NOW()
);

CREATE TABLE users (
    id         SERIAL PRIMARY KEY,
    email      VARCHAR(255) UNIQUE NOT NULL,
    password   CHAR(60) NOT NULL,
    name       VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE orders (
    id         SERIAL PRIMARY KEY,
    user_id    INT NOT NULL REFERENCES users(id),
    status     VARCHAR(20) NOT NULL DEFAULT 'pending'
               CHECK (status IN ('pending','paid','shipped','delivered','cancelled')),
    total      DECIMAL(12, 2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE order_items (
    id         SERIAL PRIMARY KEY,
    order_id   INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id INT NOT NULL REFERENCES products(id),
    quantity   INT NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(12, 2) NOT NULL,
    UNIQUE(order_id, product_id)
);
OUTPUT
실행 완료 또는 조회 결과가 표시됩니다.
🔍실행 후 확인할 것
  • 엔티티 경계각 테이블이 하나의 책임만 갖는지 확인합니다.
  • 관계 방향1:N, N:M 관계가 FK나 조인 테이블로 표현되는지 봅니다.
  • 중복 후보같은 속성이 여러 테이블에 반복되지 않는지 점검합니다.

N:M 중간 테이블의 핵심 원칙

N:M 관계를 중간 테이블로 분해할 때 지켜야 할 설계 원칙입니다. 두 FK의 조합에 UNIQUE 제약을 걸어야 같은 조합이 중복 삽입되는 것을 방지할 수 있습니다. 관계 자체에 속하는 데이터(수량, 가격 스냅샷 등)는 중간 테이블의 컬럼으로 추가합니다.

N:M 중간 테이블 설계 체크리스트:
  ✓ 양쪽 테이블의 PK를 FK로 포함
  ✓ 두 FK의 조합에 UNIQUE 제약 (중복 방지)
  ✓ 관계 자체에 속하는 속성은 중간 테이블 컬럼으로 추가
     예: order_items의 quantity, unit_price는 "주문-상품" 관계의 속성
  ✓ unit_price는 반드시 주문 시점 가격 스냅샷으로 저장
     (현재 products.price 참조 금지 — 가격 변경 시 과거 주문 금액 오염)

orders 테이블에 product_ids 컬럼을 만들고 상품 ID를 콤마로 구분해 저장했습니다. 처음에는 단순해 보였지만, 각 상품의 수량과 주문 당시 가격을 저장할 방법이 없었습니다. 결국 quantities라는 컬럼도 추가했고, 두 컬럼의 배열 인덱스를 맞춰 파싱하는 애플리케이션 코드가 생겼습니다. 특정 상품이 포함된 주문 목록을 조회하는 SQL도 LIKE '%42%' 방식이 되어 인덱스를 타지 못했고, 상품 ID 42와 142가 혼용되는 버그도 발생했습니다.

원인: 1NF를 위반하는 구조입니다. N:M 관계는 반드시 중간 테이블로 분해해야 합니다.

해결 방법: order_items 중간 테이블을 생성하고 데이터를 마이그레이션합니다. order_id + product_id 복합 UNIQUE 제약으로 중복을 방지하고, quantityunit_price를 별도 컬럼으로 저장합니다.

💼
실무 맥락신규 서비스 설계 시 ERD 먼저 그리고 팀 리뷰하는 프로세스
현업 패턴

신규 기능을 개발할 때 코드보다 ERD를 먼저 작성하는 것이 효과적입니다. dbdiagram.io나 draw.io 같은 도구로 ERD를 그린 뒤 팀 리뷰를 진행하면, 구현 이전에 잘못된 관계 설계를 발견할 수 있습니다. 예를 들어 "쿠폰은 특정 사용자에게만 발급될 수도 있고, 전체 공개될 수도 있다"는 요구사항은 ERD 리뷰에서 user_id가 NULL 허용인지 결정해야 한다는 논의로 이어집니다. 이 결정을 코드 작성 후에 하면 스키마 마이그레이션 비용이 발생합니다.

실제 팀에서 사용하는 ERD 리뷰 체크리스트는 다음과 같습니다. N:M 관계에 중간 테이블이 있는가? 모든 엔티티에 PK가 있는가? ON DELETE 정책이 비즈니스 규칙과 일치하는가? 자주 조인되는 컬럼에 인덱스가 있는가? 이 체크리스트를 통과한 ERD가 SQL DDL 작성의 출발점이 됩니다.

다음 모듈에서는 테이블, 스키마, 컬럼 등 데이터베이스 핵심 용어와 계층 구조를 다룹니다.

지식 확인

퀴즈 — 5문제

Q1

ERD에서 users → orders 연결선을 보니 orders 쪽에 '원(O)과 까마귀발' 기호가 붙어 있다. 이 기호가 나타내는 카디널리티는?

Q2

학생과 수업의 관계에서 '한 학생은 여러 수업을 듣고, 한 수업에는 여러 학생이 있다'는 어떤 관계인가?

Q3

N:M 관계를 관계형 DB에서 표현하는 올바른 방법은?

Q4

쇼핑몰 DB 설계 중, '배송 상태 변경 이력(택배사 API가 보내주는 시각·위치·상태코드)'을 별도 테이블로 분리해야 할지 고민이다. 엔티티로 분리할 기준으로 가장 적절한 것은?

Q5

ERD 설계 시 '주문 아이템(OrderItem)'이 '주문(Order)'을 외래키로 반드시 참조해야 하는 이유로 가장 적절한 것은?

0 / 5 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

Ubuntu 서버에 PostgreSQL을 설치하고, 데이터베이스와 사용자를 생성한 뒤 외부 접속이 가능하도록 설정한다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

database입문 · 40
[Database] 정밀한 데이터 타입(숫자·문자·날짜) 선택 기준
Database 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점