🗄️ DatabaseDevGuide0004
데이터베이스 설계 완전 정복 A-Z
좋은 DB를 설계해야 좋은 서비스가 나온다!
현업에서 바로 쓰는 DB 설계의 모든 것을 A-Z로 배워봅시다
📋 학습 목차
| 챕터 | 주제 | 핵심 키워드 |
|---|---|---|
| 01 | DB 설계란 무엇인가? | 설계 3단계, 개념/논리/물리 모델 |
| 02 | 기본키(PK)와 외래키(FK) | PRIMARY KEY, FOREIGN KEY, 무결성 |
| 03 | ERD 다이어그램 설계 | 엔티티, 관계, 카디널리티, ERD 도구 |
| 04 | 정규화(Normalization) | 1NF, 2NF, 3NF, BCNF |
| 05 | 반정규화(Denormalization) | 성능 vs 중복, 실무 판단 기준 |
| 06 | 데이터 타입 선택 | INT, VARCHAR, DATETIME, JSON, ENUM |
| 07 | 제약조건(Constraints) | NOT NULL, UNIQUE, CHECK, DEFAULT |
| 08 | 관계 유형(1:1, 1:N, M:N) | 관계 모델링, 중간 테이블 설계 |
| 09 | 실전 프로젝트: 쇼핑몰 DB 설계 | 요구사항 분석 → ERD → DDL → 검증 |
| 10 | 나쁜 설계 vs 좋은 설계 | 안티패턴, 설계 체크리스트 |
| 11 | 현업 면접 Q&A | 설계 관련 면접 질문 TOP 10 |
| 12 | 학습 로드맵 & 체크리스트 | 단계별 학습 경로, 추천 도구 |
📌 01. 데이터베이스 설계란 무엇인가?
설계를 잘못하면 나중에 수정이 거의 불가능합니다. 처음부터 잘 설계하는 게 핵심!
🏗️ DB 설계의 3단계
건물을 짓는 것과 같습니다:
개념 설계 = 어떤 방이 필요한지 결정 (거실, 주방, 방 3개)
논리 설계 = 방의 배치도 그리기 (도면 작성)
물리 설계 = 실제 벽돌 쌓고 공사하기 (시공)
🎯 좋은 DB 설계의 5가지 원칙
같은 데이터를 여러 곳에 저장하지 않기
데이터가 항상 올바른 상태 유지
나중에 기능 추가가 쉬운 구조
조회/수정이 빠른 구조
다른 개발자도 쉽게 파악 가능한 명명
📌 02. 기본키(PK)와 외래키(FK)
테이블 간의 관계를 연결하는 핵심 개념!
🔑 기본키 (PRIMARY KEY)
기본키는 테이블에서 각 행(Row)을 유일하게 식별하는 컬럼입니다.
학생 테이블이라면 "학번"이 기본키 — 두 학생이 같은 학번을 가질 수 없죠!
- NOT NULL: 반드시 값이 있어야 함
- UNIQUE: 중복 값 허용 안 함
- 변경 불가: 되도록 한번 정하면 바꾸지 않는 게 원칙
- 최소 컬럼: 가능하면 단일 컬럼으로 (복합키는 복잡도 증가)
-- ✅ 기본키 설정 예시
CREATE TABLE users (
user_id INT NOT NULL AUTO_INCREMENT, -- 자동 증가 숫자 PK (가장 많이 사용)
email VARCHAR(255) NOT NULL,
username VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id) -- 기본키 선언
);
-- ✅ UUID를 기본키로 사용 (분산 시스템에서 많이 사용)
CREATE TABLE orders (
order_id CHAR(36) NOT NULL DEFAULT (UUID()),
user_id INT NOT NULL,
total_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id)
);
-- ✅ 복합 기본키 (두 컬럼의 조합이 유일한 경우)
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id) -- 복합키
);
🔗 외래키 (FOREIGN KEY)
외래키는 다른 테이블의 기본키를 참조하는 컬럼입니다.
주문 테이블의 "user_id"가 사용자 테이블의 "user_id"를 참조하는 것이 외래키!
참조 무결성: 외래키의 값은 반드시 참조 테이블에 존재하는 값이어야 합니다.
없는 사용자 ID로 주문을 넣으려 하면? → DB가 자동으로 막아줍니다! 🛡️
-- ✅ 외래키 설정 예시
CREATE TABLE orders (
order_id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL, -- users.user_id를 참조
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (order_id),
FOREIGN KEY (user_id)
REFERENCES users(user_id)
ON DELETE CASCADE -- 사용자 삭제 시 주문도 같이 삭제
ON UPDATE CASCADE -- 사용자 ID 변경 시 주문의 user_id도 자동 변경
);
⚙️ ON DELETE / ON UPDATE 옵션 비교
- 외래키가 많으면 INSERT/DELETE 성능이 저하될 수 있습니다
- 대규모 서비스에서는 외래키 없이 애플리케이션 레벨에서 무결성을 관리하기도 합니다
- 카카오, 쿠팡 같은 대형 서비스는 외래키 제약을 DB 레벨에서 비활성화하는 경우도 많음
- 그래도 처음 배울 때는 외래키를 쓰는 것이 올바른 설계 습관 형성에 도움!
📌 03. ERD 다이어그램 설계
ERD (Entity-Relationship Diagram) = 데이터베이스의 설계도!
📐 ERD의 3가지 핵심 구성요소
엔티티 (Entity)
저장할 대상
예: 사용자, 주문, 상품
네모(□)로 표현
관계 (Relationship)
엔티티 간의 연결
예: 사용자가 주문을 "한다"
마름모(◇)로 표현
속성 (Attribute)
엔티티의 특성/정보
예: 이름, 나이, 이메일
타원(○)으로 표현
📊 카디널리티 (Cardinality) - 관계의 수
🔄 M:N 관계를 해결하는 중간 테이블
⚡ 왜 중간 테이블이 필요한가?
학생이 여러 수업을 들을 수 있고, 수업에는 여러 학생이 있습니다.
이를 직접 연결하면 데이터 구조가 무너집니다. 중간 테이블이 해결책!
-- ❌ M:N 직접 표현 불가능
-- student 테이블에 수업 목록을 어디에 저장? → 불가!
-- ✅ 중간 테이블(연결 테이블)로 해결
CREATE TABLE students (
student_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL
);
CREATE TABLE courses (
course_id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL
);
-- 중간 테이블: 학생-수업 등록 관계
CREATE TABLE enrollments (
enrollment_id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL, -- students.student_id 참조
course_id INT NOT NULL, -- courses.course_id 참조
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
grade CHAR(2), -- 중간 테이블에 추가 속성도 넣을 수 있음!
FOREIGN KEY (student_id) REFERENCES students(student_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id),
UNIQUE (student_id, course_id) -- 같은 학생이 같은 수업을 중복 등록 방지
);
🛠️ ERD 설계 도구 비교
📝 dbdiagram.io 사용 예시 (DBML 코드)
// dbdiagram.io 에서 이렇게 작성하면 ERD가 자동으로 그려집니다!
Table users {
user_id int [pk, increment]
email varchar [not null, unique]
username varchar [not null]
created_at datetime
}
Table orders {
order_id int [pk, increment]
user_id int [ref: > users.user_id] // FK 표기
total_price decimal
status varchar
}
Table order_items {
item_id int [pk, increment]
order_id int [ref: > orders.order_id]
product_id int [ref: > products.product_id]
quantity int
price decimal
}
Table products {
product_id int [pk, increment]
name varchar
price decimal
stock int
}
📌 04. 정규화 (Normalization)
데이터 중복을 없애고, 이상(Anomaly)을 방지하는 테이블 재구성 과정!
정규화를 하지 않으면 3가지 이상(Anomaly) 문제가 발생합니다:
삽입 이상: 원하지 않는 데이터도 함께 넣어야 함
수정 이상: 한 데이터를 수정할 때 여러 곳을 수정해야 함
삭제 이상: 하나를 삭제하면 다른 필요한 데이터도 같이 사라짐
📋 정규화 전: 문제가 있는 테이블
⬆️ 빨간 부분: 같은 주문인데 고객정보가 중복! 고객 주소 변경 시 2곳 수정 필요 → 수정 이상!
✅ 제1정규형 (1NF) - 원자값 원칙
규칙: 모든 컬럼의 값은 원자값(더 이상 쪼갤 수 없는 단일 값)이어야 합니다.
예: "전화번호" 컬럼에 "010-1234, 010-5678" 처럼 여러 값을 쉼표로 넣으면 안 됩니다!
-- ❌ 1NF 위반: 한 컬럼에 여러 값
CREATE TABLE bad_users (
user_id INT,
name VARCHAR(100),
phones VARCHAR(500) -- "010-1234-5678, 010-9876-5432" 저장 → 위반!
);
-- ✅ 1NF 준수: 별도 테이블로 분리
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE user_phones (
phone_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
phone VARCHAR(20) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
✅ 제2정규형 (2NF) - 부분 함수 종속 제거
조건: 1NF를 만족하면서, 기본키가 복합키일 때 기본키의 일부에만 종속되는 컬럼이 없어야 합니다.
쉽게: 복합키가 있을 때, 키의 일부분으로도 결정되는 컬럼은 분리하세요!
-- ❌ 2NF 위반
-- order_items(order_id, product_id) 복합키인데
-- product_name은 product_id에만 종속! → 분리 필요
CREATE TABLE bad_order_items (
order_id INT,
product_id INT,
quantity INT,
product_name VARCHAR(200), -- product_id에만 종속 → 2NF 위반!
product_price DECIMAL(10,2), -- product_id에만 종속 → 2NF 위반!
PRIMARY KEY (order_id, product_id)
);
-- ✅ 2NF 준수: 상품 정보는 products 테이블로 분리
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(200),
price DECIMAL(10,2)
);
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
✅ 제3정규형 (3NF) - 이행적 함수 종속 제거
규칙: 2NF를 만족하면서, 기본키가 아닌 컬럼이 다른 기본키 아닌 컬럼에 종속되면 안 됩니다.
쉽게: A→B→C 처럼 "기본키→중간컬럼→다른컬럼" 형태를 제거하세요!
-- ❌ 3NF 위반
-- user_id → department_id → department_name (이행적 종속!)
CREATE TABLE bad_employees (
user_id INT PRIMARY KEY,
name VARCHAR(100),
department_id INT,
department_name VARCHAR(100) -- department_id에 종속 → 3NF 위반!
);
-- ✅ 3NF 준수: 부서 정보를 별도 테이블로 분리
CREATE TABLE departments (
department_id INT PRIMARY KEY,
department_name VARCHAR(100)
);
CREATE TABLE employees (
user_id INT PRIMARY KEY,
name VARCHAR(100),
department_id INT,
FOREIGN KEY (department_id) REFERENCES departments(department_id)
);
📊 정규화 단계 요약표
실무에서는 대부분 3NF까지만 정규화합니다. BCNF 이상은 학문적 개념으로, 실무에서 직접 적용하는 경우는 드뭅니다. 면접에서는 1NF~3NF까지 설명할 수 있으면 충분합니다!
📌 05. 반정규화 (Denormalization)
정규화를 의도적으로 "역행"해서 성능을 높이는 전략!
✅ 정규화 장점
- 데이터 중복 없음
- 수정/삭제 쉬움
- 무결성 보장
⚠️ 정규화 단점
- JOIN 쿼리 복잡
- 조회 성능 저하
- 쿼리 작성 어려움
🔧 반정규화 기법 3가지
① 컬럼 중복 추가 (Derived Column)
JOIN 없이 바로 조회할 수 있도록 자주 쓰이는 값을 다른 테이블에 복사
-- ✅ 반정규화: orders 테이블에 user_name 컬럼 추가
-- 매번 users JOIN 없이 주문 목록에 고객명 바로 표시 가능
ALTER TABLE orders ADD COLUMN user_name VARCHAR(100); -- 중복이지만 성능 향상
-- 단점: users.name 변경 시 orders.user_name도 함께 업데이트 필요!
② 집계 컬럼 추가 (Summary Column)
매번 COUNT/SUM 집계를 계산하지 않고 미리 저장
-- ✅ 반정규화: 게시글 테이블에 댓글 수 컬럼 미리 저장
ALTER TABLE posts ADD COLUMN comment_count INT DEFAULT 0;
-- 댓글 추가 시 자동 업데이트 (트리거 활용)
CREATE TRIGGER trg_comment_count_inc
AFTER INSERT ON comments
FOR EACH ROW
UPDATE posts SET comment_count = comment_count + 1 WHERE post_id = NEW.post_id;
③ 테이블 병합 (Table Merging)
1:1 관계 테이블을 하나로 합치기
-- users + user_profiles를 합쳐서 JOIN 제거
CREATE TABLE users (
user_id INT PRIMARY KEY,
email VARCHAR(255),
-- 아래는 원래 user_profiles 테이블 내용
bio TEXT,
profile_img VARCHAR(500),
birthday DATE
);
🎯 반정규화 언제 해야 하나?
📌 06. 데이터 타입 선택
올바른 데이터 타입 선택 = 저장 공간 절약 + 성능 향상
📊 MySQL 주요 데이터 타입 완전 정리
🔢 숫자형 타입
🔤 문자형 타입
📅 날짜/시간형 타입
-- ✅ 데이터 타입 올바른 사용 예시
CREATE TABLE products (
product_id INT UNSIGNED NOT NULL AUTO_INCREMENT, -- UNSIGNED: 양수만 허용 (0~42억)
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL, -- 금액은 반드시 DECIMAL!
stock SMALLINT UNSIGNED DEFAULT 0, -- 재고는 음수 없으니 UNSIGNED
status TINYINT(1) DEFAULT 1, -- 0=비활성, 1=활성 (bool 대용)
description TEXT,
thumbnail_url VARCHAR(500),
category ENUM('전자제품','의류','식품','가구'), -- 고정 선택지
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id)
);
📌 07. 제약조건 (Constraints)
데이터 무결성을 지키는 DB 레벨의 수문장!
🛡️ 6가지 제약조건 완전 정리
-- ✅ 제약조건 종합 예시
CREATE TABLE employees (
emp_id INT NOT NULL AUTO_INCREMENT,
emp_code CHAR(6) NOT NULL, -- 사번 형식 고정 6자리
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
age INT,
salary DECIMAL(10,2) NOT NULL,
dept_id INT,
hired_at DATE NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (emp_id),
UNIQUE KEY uk_emp_code (emp_code), -- 사번 중복 불가
UNIQUE KEY uk_email (email), -- 이메일 중복 불가
CHECK (age >= 18 AND age <= 70), -- 나이 범위 제한
CHECK (salary > 0), -- 급여는 양수
FOREIGN KEY (dept_id) REFERENCES departments(dept_id) ON DELETE SET NULL
);
💡 제약조건 후에 추가하는 방법 (ALTER TABLE)
-- 이미 만든 테이블에 제약조건 추가
ALTER TABLE users ADD CONSTRAINT uk_phone UNIQUE (phone);
ALTER TABLE orders ADD CONSTRAINT chk_total CHECK (total_price >= 0);
ALTER TABLE products ADD FOREIGN KEY (category_id) REFERENCES categories(category_id);
-- 제약조건 삭제
ALTER TABLE users DROP INDEX uk_phone;
ALTER TABLE orders DROP CHECK chk_total;
📌 08. 관계 유형 심화 (1:1, 1:N, M:N)
각 관계 유형별 실제 테이블 설계 패턴을 익혀봅시다!
🔗 1:1 관계 설계 패턴
언제 1:1로 분리?
① 자주 접근하는 기본 정보 vs 드물게 쓰는 상세 정보 분리 (성능 최적화)
② 보안상 민감한 정보를 별도 테이블에 분리 (접근 제어)
③ 선택적으로 존재하는 정보 (모든 사용자에게 프로필이 없을 수 있음)
-- ✅ 1:1 관계: 사용자 + 사용자 프로필
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, -- 자주 쓰는 기본 정보
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_profiles (
profile_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT UNIQUE NOT NULL, -- UNIQUE → 1:1 보장!
nickname VARCHAR(100),
bio TEXT,
avatar_url VARCHAR(500),
website VARCHAR(300),
birthday DATE,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
-- 조회: 1:1이라 항상 최대 1개 행만 JOIN됨
SELECT u.email, p.nickname, p.bio
FROM users u
LEFT JOIN user_profiles p ON u.user_id = p.user_id
WHERE u.user_id = 1;
🔗 1:N 관계 설계 패턴
가장 흔한 패턴! 부모 테이블의 PK를 자식 테이블의 FK로 저장합니다.
예: 1명의 사용자가 N개의 주문, 1개의 카테고리에 N개의 상품
-- ✅ 1:N 관계: 사용자(1) : 주문(N)
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL
);
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL, -- 1:N의 N쪽에 FK 저장
status ENUM('주문','결제','배송','완료','취소') DEFAULT '주문',
total_price DECIMAL(10,2) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT
);
-- 조회: 특정 사용자의 모든 주문
SELECT o.order_id, o.status, o.total_price
FROM orders o
WHERE o.user_id = 1
ORDER BY o.created_at DESC;
🔗 M:N 관계 설계 패턴 (심화)
M:N 관계는 반드시 중간 테이블로 해결!
중간 테이블에는 두 테이블의 FK 외에도 관계에 대한 속성을 추가할 수 있습니다.
예: 학생-수업 관계에서 "성적", "수강신청일" 같은 부가 정보
-- ✅ M:N 관계: 상품(M) : 태그(N)
CREATE TABLE products (
product_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL
);
CREATE TABLE tags (
tag_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) UNIQUE NOT NULL
);
-- 중간 테이블: 상품-태그 연결
CREATE TABLE product_tags (
product_id INT NOT NULL,
tag_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (product_id, tag_id), -- 복합 PK로 중복 방지
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE
);
-- 조회: 특정 상품의 태그 목록
SELECT t.name
FROM product_tags pt
JOIN tags t ON pt.tag_id = t.tag_id
WHERE pt.product_id = 1;
-- 조회: 특정 태그가 달린 상품 목록
SELECT p.name
FROM product_tags pt
JOIN products p ON pt.product_id = p.product_id
WHERE pt.tag_id = (SELECT tag_id FROM tags WHERE name = '신상품');
🌳 자기 참조 (Self-Referencing) 관계
같은 테이블 내에서 부모-자식 관계를 표현할 때 사용합니다.
예: 카테고리(대분류→중분류→소분류), 직원(상사→부하), 댓글(댓글→대댓글)
-- ✅ 자기 참조: 카테고리 계층 구조
CREATE TABLE categories (
category_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id INT DEFAULT NULL, -- 자기 자신을 참조
depth TINYINT DEFAULT 0, -- 계층 깊이 (0=대분류, 1=중분류...)
FOREIGN KEY (parent_id) REFERENCES categories(category_id) ON DELETE SET NULL
);
-- 데이터 예시
INSERT INTO categories VALUES (1, '전자제품', NULL, 0); -- 대분류
INSERT INTO categories VALUES (2, '컴퓨터', 1, 1); -- 중분류 (전자제품의 자식)
INSERT INTO categories VALUES (3, '노트북', 2, 2); -- 소분류 (컴퓨터의 자식)
-- 특정 카테고리의 하위 카테고리 조회
SELECT c.name, parent.name AS parent_name
FROM categories c
LEFT JOIN categories parent ON c.parent_id = parent.category_id
WHERE c.parent_id = 1;
📌 09. 실전 프로젝트 - 쇼핑몰 DB 설계 A-Z
요구사항 분석 → ERD 설계 → DDL 작성 → 검증까지 전 과정 실습!
📋 Step 1. 요구사항 분석
쇼핑몰 서비스의 요구사항:
- 사용자는 회원가입/로그인을 할 수 있다 → users 테이블
- 사용자는 배송지를 여러 개 저장할 수 있다 → addresses 테이블
- 상품은 카테고리별로 분류된다 → categories, products 테이블
- 사용자는 장바구니에 상품을 담을 수 있다 → cart_items 테이블
- 주문 시 여러 상품을 한 번에 구매할 수 있다 → orders, order_items 테이블
- 상품에 리뷰를 달 수 있다 → reviews 테이블
- 상품은 여러 태그를 가질 수 있다 (검색용) → tags, product_tags 테이블
📐 Step 2. ERD 구조 (텍스트 표현)
users (1) ──────── (N) addresses [사용자:배송지 = 1:N]
users (1) ──────── (N) orders [사용자:주문 = 1:N]
users (1) ──────── (N) cart_items [사용자:장바구니 = 1:N]
users (1) ──────── (N) reviews [사용자:리뷰 = 1:N]
categories (1) ─── (N) products [카테고리:상품 = 1:N]
products (1) ────── (N) cart_items [상품:장바구니 = 1:N]
products (1) ────── (N) order_items [상품:주문상세 = 1:N]
products (1) ────── (N) reviews [상품:리뷰 = 1:N]
products (M) ────── (N) tags [상품:태그 = M:N → product_tags]
orders (1) ─────── (N) order_items [주문:주문상세 = 1:N]
orders (N) ─────── (1) addresses [주문:배송지 = N:1]
🏗️ Step 3. DDL - 전체 테이블 생성
-- 쇼핑몰 DB 생성
CREATE DATABASE shopdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE shopdb;
-- 1. 사용자 테이블
CREATE TABLE users (
user_id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
is_active TINYINT(1) NOT NULL DEFAULT 1,
role ENUM('customer','admin') NOT NULL DEFAULT 'customer',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (user_id),
UNIQUE KEY uk_email (email)
);
-- 2. 배송지 테이블
CREATE TABLE addresses (
address_id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
alias VARCHAR(50) DEFAULT '집', -- '집', '회사' 등
recipient VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
zipcode CHAR(5) NOT NULL,
address1 VARCHAR(300) NOT NULL,
address2 VARCHAR(200),
is_default TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (address_id),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
-- 3. 카테고리 테이블 (자기 참조)
CREATE TABLE categories (
category_id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id BIGINT DEFAULT NULL,
sort_order INT DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (category_id),
FOREIGN KEY (parent_id) REFERENCES categories(category_id) ON DELETE SET NULL
);
-- 4. 상품 테이블
CREATE TABLE products (
product_id BIGINT NOT NULL AUTO_INCREMENT,
category_id BIGINT NOT NULL,
name VARCHAR(300) NOT NULL,
description TEXT,
price DECIMAL(12,2) NOT NULL,
sale_price DECIMAL(12,2), -- 할인가 (NULL이면 할인 없음)
stock INT NOT NULL DEFAULT 0,
status ENUM('판매중','품절','단종') NOT NULL DEFAULT '판매중',
thumbnail VARCHAR(500),
view_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id),
FOREIGN KEY (category_id) REFERENCES categories(category_id),
INDEX idx_category (category_id),
INDEX idx_status (status),
INDEX idx_price (price)
);
-- 5. 태그 테이블
CREATE TABLE tags (
tag_id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) UNIQUE NOT NULL,
PRIMARY KEY (tag_id)
);
-- 6. 상품-태그 연결 테이블 (M:N)
CREATE TABLE product_tags (
product_id BIGINT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (product_id, tag_id),
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE
);
-- 7. 장바구니 테이블
CREATE TABLE cart_items (
cart_id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cart_id),
UNIQUE KEY uk_user_product (user_id, product_id), -- 같은 상품 중복 방지
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE
);
-- 8. 주문 테이블
CREATE TABLE orders (
order_id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
address_id BIGINT,
total_price DECIMAL(12,2) NOT NULL,
discount_price DECIMAL(12,2) NOT NULL DEFAULT 0,
final_price DECIMAL(12,2) NOT NULL,
status ENUM('주문접수','결제완료','배송준비','배송중','배송완료','취소','반품') NOT NULL DEFAULT '주문접수',
memo VARCHAR(500),
ordered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (order_id),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT,
FOREIGN KEY (address_id) REFERENCES addresses(address_id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_ordered_at (ordered_at)
);
-- 9. 주문 상세 테이블
CREATE TABLE order_items (
item_id BIGINT NOT NULL AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT,
name VARCHAR(300) NOT NULL, -- 주문 당시 상품명 (상품 삭제돼도 유지)
price DECIMAL(12,2) NOT NULL, -- 주문 당시 가격 (나중에 가격 변경돼도 유지)
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (item_id),
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE SET NULL
);
-- 10. 리뷰 테이블
CREATE TABLE reviews (
review_id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
order_id BIGINT,
rating TINYINT NOT NULL,
content TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (review_id),
CHECK (rating BETWEEN 1 AND 5),
UNIQUE KEY uk_user_product_order (user_id, product_id, order_id), -- 주문 1번당 리뷰 1개
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE SET NULL
);
🔍 Step 4. 검증 쿼리
-- ✅ 사용자별 총 주문금액 집계
SELECT u.name, COUNT(o.order_id) AS order_count, SUM(o.final_price) AS total_spent
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND o.status != '취소'
GROUP BY u.user_id, u.name
ORDER BY total_spent DESC;
-- ✅ 카테고리별 상품 수 및 평균 가격
SELECT c.name AS category, COUNT(p.product_id) AS product_count,
ROUND(AVG(p.price),0) AS avg_price
FROM categories c
LEFT JOIN products p ON c.category_id = p.category_id
GROUP BY c.category_id, c.name
ORDER BY product_count DESC;
-- ✅ 인기 상품 TOP 10 (주문 기준)
SELECT p.name, COUNT(oi.item_id) AS order_count, SUM(oi.quantity) AS total_qty
FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = '배송완료'
GROUP BY p.product_id, p.name
ORDER BY total_qty DESC
LIMIT 10;
-- ✅ 평점 4점 이상 상품 목록
SELECT p.name, ROUND(AVG(r.rating),1) AS avg_rating, COUNT(r.review_id) AS review_count
FROM products p
JOIN reviews r ON p.product_id = r.product_id
GROUP BY p.product_id, p.name
HAVING avg_rating >= 4.0
ORDER BY avg_rating DESC, review_count DESC;
🎯 설계 시 중요한 포인트 3가지
- order_items에 상품명/가격을 별도 저장: 나중에 상품이 삭제되거나 가격이 바뀌어도 주문 내역은 유지되어야 함
- soft delete 고려: is_active 컬럼으로 물리 삭제 대신 논리 삭제 (데이터 복구 가능)
- BIGINT 사용: 대규모 서비스에서 INT의 한계(21억)를 넘을 수 있으므로 ID는 BIGINT로
📌 10. 나쁜 설계 vs 좋은 설계 (안티패턴)
현업에서 흔히 보이는 잘못된 설계 패턴과 올바른 해결책!
❌ 안티패턴 1: 만능 테이블 (God Table)
❌ 나쁜 설계
-- 모든 정보를 한 테이블에!
CREATE TABLE all_in_one (
id INT,
type VARCHAR(50), -- 'user','product','order'
field1 VARCHAR(500),
field2 VARCHAR(500),
field3 VARCHAR(500),
data TEXT -- JSON으로 모든 데이터 때려넣기
);
✅ 좋은 설계
-- 역할에 맞게 테이블 분리
CREATE TABLE users (...);
CREATE TABLE products (...);
CREATE TABLE orders (...);
-- 각 테이블은 단일 책임!
-- 필요 시 JOIN으로 연결
❌ 안티패턴 2: 컬럼이름에 번호 붙이기 (Repeated Columns)
❌ 나쁜 설계
-- 배송지를 컬럼으로 여러 개!
CREATE TABLE users (
user_id INT,
addr1 VARCHAR(300),
addr2 VARCHAR(300),
addr3 VARCHAR(300),
phone1 VARCHAR(20),
phone2 VARCHAR(20)
);
✅ 좋은 설계
-- 별도 테이블로 1:N 관계
CREATE TABLE addresses (
addr_id INT PRIMARY KEY,
user_id INT NOT NULL, -- FK
address VARCHAR(300),
phone VARCHAR(20),
...
);
❌ 안티패턴 3: NULL 남용
❌ 나쁜 설계
-- 모든 컬럼을 nullable로
CREATE TABLE orders (
order_id INT,
status VARCHAR(50), -- NULL이면 접수전?취소?
price DECIMAL, -- NULL이면 무료?미확정?
note VARCHAR(500) -- OK: 이건 nullable 적절
);
✅ 좋은 설계
-- 필수값은 NOT NULL + DEFAULT
CREATE TABLE orders (
order_id INT PRIMARY KEY,
status VARCHAR(50) NOT NULL DEFAULT '접수',
price DECIMAL NOT NULL,
note VARCHAR(500) -- 선택값만 nullable
);
📋 DB 설계 최종 체크리스트
📌 11. 현업 면접 Q&A TOP 10
DB 설계 관련 실제 면접에서 자주 나오는 질문들!
📌 12. 학습 로드맵 & 체크리스트
1주차
PK, FK, 제약조건
기본 개념 이해
2주차
ERD 설계 실습
dbdiagram.io 활용
3주차
정규화 연습
1NF~3NF 직접 적용
4주차
미니 프로젝트
쇼핑몰 DB 직접 설계
5주차
면접 Q&A 정리
설계 설명 연습
🛠️ 추천 학습 도구
🎉 DatabaseDevGuide0004 완료!
DB 설계의 기본기를 완전히 익혔습니다!
좋은 설계는 좋은 서비스의 시작입니다. ERD를 직접 그리고 DDL로 구현해보세요!
📌 다음 단계: DatabaseDevGuide0005 - NoSQL 입문
(MongoDB, Redis, 언제 SQL vs NoSQL?)