모델링 종합 실습 —
요구사항에서 ERD까지
섹션 4의 마지막 강의입니다. 새로운 이론은 없어요. 대신 지금까지 배운 판단을 한 번에 사용합니다. 기존 쇼핑몰 DB에 “회원이 상품에 별점과 후기를 남기는 기능”을 추가해 볼 거예요 — 요구사항을 질문으로 채우고, 테이블과 관계를 정하고, 작은 사례로 검산하는 데까지 직접 완성합니다.
이번 섹션의 여정
완료 ✓
완료 ✓
지금 여기
설계한 구조에서 꺼내기
오늘 만들 것 — 상품 후기 기능
우리 shop에는 회원, 카테고리, 상품, 주문, 주문상세 다섯 테이블이 있습니다. 여기에 기능 하나를 얹어 볼게요.
이번에 설계할 product_reviews 는 실습 DB shop 에 존재하지 않는 새
테이블입니다. 지금 DBeaver 를 열어도 보이지 않아요. 다만 이 테이블이 연결될
members 와 products 는 실제 스키마 그대로이므로, 관계와 키는 진짜 구조에
맞춰 정합니다.
이 문장 하나만 들고 바로 테이블을 그리기 시작하면 — 거의 반드시 다시 고치게 됩니다. 순서를 지켜서 가겠습니다.
① 요구사항의 빈칸부터 질문합니다
“회원이 상품에 후기를 남긴다” 는 문장만으로는 설계할 수 없습니다. 이 문장에는 정해지지 않은 것이 너무 많아요. 그러니 테이블을 만들기 전에 업무 담당자에게 물어봐야 합니다.
이번 실습에서는 담당자에게 이렇게 확인받았다고 하겠습니다.
| 질문 | 이번 실습의 결정 |
|---|---|
| 같은 회원이 같은 상품에 여러 번 작성? | 최대 한 번 · 이후에는 기존 후기를 수정 |
| 별점 | 필수 · 1 ~ 5 사이의 정수 |
| 후기 글 | 선택 입력 — 비워도 된다 |
| 시각 | 작성 시각 · 수정 시각을 기록 |
| 후기 0개인 회원 · 상품 | 허용 |
그리고 이번 범위에서 정하지 않은 것도 있습니다. 이것을 따로 적어두는 게 중요해요.
| 항목 | 이번 버전의 처리 |
|---|---|
| 구매자만 후기를 쓸 수 있는가? | 이번 버전에서는 조건으로 삼지 않는다 |
| 후기 삭제 정책 | 다음 범위로 미룬다 |
| 후기 이미지 첨부 | 이번 범위 밖 |
확인한 규칙과 아직 정하지 않은 규칙을 섞지 않는 것 — 이것이 설계의 첫 단계입니다. 섞이는 순간, 내가 임의로 가정한 내용이 요구사항인 척하기 시작해요.
② 새 테이블과 한 행의 의미를 정합니다
회원 정보는 members에, 상품 정보는 products에 이미 있습니다. 새로 필요한 건 회원과 상품 사이에 생긴 후기라는 기록이에요. 그래서 테이블 하나를 새로 만듭니다 — product_reviews.
그림보다 먼저 정할 것은 4-1에서 배운 그것입니다.
한 행의 의미가 정해지면 어떤 값이 필요한지가 따라 나옵니다.
| 컬럼 | 뜻 | 규칙 |
|---|---|---|
| review_id | 후기 번호 | 기본키 |
| member_id | 작성한 회원 번호 | 외래키 · 필수 |
| product_id | 평가한 상품 번호 | 외래키 · 필수 |
| rating | 별점 | 정수 1 ~ 5 · 필수 |
| review_content | 후기 내용 | 선택 — 비워도 된다 |
| created_at | 작성 시각 | 필수 |
| updated_at | 수정 시각 | 아직 수정하지 않았다면 비어 있다 |
여기서 넣고 싶어지는 컬럼이 하나씩 떠오를 거예요. 후기 목록에 회원 이름이랑 상품 이름도 같이 보여야 하는데, member_name이랑 product_name도 넣어둘까? — 4-5에서 다룬 그 유혹입니다.
회원 이름은 members 의 사실이고, 상품 이름은 products 의 사실입니다.
후기에 복사해 두면 회원이 이름을 바꾸는 순간 두 곳의 값이 달라져요. 후기
화면에 함께 보여주는 건 저장이 아니라 조회의 일입니다 — 다음 섹션에서
배울 JOIN 이 그 역할을 합니다.
반대로 rating, review_content, created_at은 그 후기에만 속한 사실입니다. 다른 어느 테이블에도 주인이 없어요. 그래서 여기에 둡니다.
③ 키 · 관계 · 제약을 업무 문장에서 옮깁니다
먼저 키입니다. 후기 한 건은 review_id로 구별합니다. member_id는 기존 회원을, product_id는 기존 상품을 가리키는 외래키예요.
다음은 관계입니다. 4-2에서 배운 대로 양쪽 방향에서 문장을 만들어 봅니다.
| 방향 | 읽은 문장 | 표기 |
|---|---|---|
| 회원 → 후기 | 회원 한 명은 후기가 없을 수도, 여러 개일 수도 있다 | 0..N |
| 후기 → 회원 | 후기 한 건에는 작성자가 정확히 한 명 있다 | 1..1 |
| 상품 → 후기 | 상품 하나는 후기가 없을 수도, 여러 개가 달릴 수도 있다 | 0..N |
| 후기 → 상품 | 후기 한 건은 상품 정확히 하나에 속한다 | 1..1 |
order_items가 orders와 products 두 부모를 참조했던 것과 같은 모양이에요. product_reviews도 부모가 둘입니다.
members 1..1 ───── 0..N product_reviews 0..N ───── 1..1 products
product_reviews.member_id → members.member_id
product_reviews.product_id → products.product_id그런데 여기서 멈추면 요구사항 하나가 빠집니다.
“한 회원은 같은 상품에 후기를 최대 한 번” 은 관계선만으로 지켜지지 않습니다. 지금 구조는 김철수 회원이 상품 23번에 후기를 열 개 써도 아무 문제 없이 저장해요. 두 외래키는 “존재하는 회원·상품인가”만 검사하기 때문입니다.
그래서 관계선 밖의 제약을 따로 적습니다.
| 제약 | 업무 문장으로 읽으면 |
|---|---|
| UNIQUE (member_id, product_id) | 같은 회원 번호와 상품 번호의 조합은 두 번 저장할 수 없다 |
| rating — 정수 1 ~ 5 · NOT NULL | 별점은 1점부터 5점 사이의 정수이고 반드시 입력한다 |
| review_content — NULL 허용 | 후기 글은 비워도 된다 |
(member_id, product_id) 를 기본키로 잡을 수도 있습니다. 다만 후기는 나중에
“후기 12번”처럼 한 건을 짧게 가리킬 일이 많아서, 여기서는 review_id 를
기본키로 두고 중복 금지는 UNIQUE 로 따로 걸었습니다. 어느 쪽이든 “같은
조합은 한 번뿐”이라는 규칙은 지켜져요.
설계 결정이 코드로 어떻게 옮겨지는지
지금까지의 결정을 그대로 CREATE TABLE로 옮기면 이렇게 됩니다. 3-6에서 읽는 법을 배웠으니 — 문법을 외우려 하지 말고, 위에서 내린 결정이 어느 줄이 되었는지만 따라가 보세요.
CREATE TABLE product_reviews (
review_id INT NOT NULL AUTO_INCREMENT,
member_id INT NOT NULL,
product_id INT NOT NULL,
rating INT NOT NULL,
review_content TEXT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NULL,
PRIMARY KEY (review_id),
UNIQUE KEY uq_review_member_product (member_id, product_id),
CONSTRAINT chk_review_rating CHECK (rating BETWEEN 1 AND 5),
CONSTRAINT fk_review_member
FOREIGN KEY (member_id) REFERENCES members(member_id),
CONSTRAINT fk_review_product
FOREIGN KEY (product_id) REFERENCES products(product_id)
);| 우리가 내린 결정 | 코드에서는 이 줄 |
|---|---|
| 후기 한 건을 구별한다 | PRIMARY KEY (review_id) |
| 작성자는 기존 회원 한 명 | FOREIGN KEY (member_id) REFERENCES members(member_id) |
| 대상은 기존 상품 하나 | FOREIGN KEY (product_id) REFERENCES products(product_id) |
| 같은 회원·상품 조합은 한 번만 | UNIQUE KEY uq_review_member_product (member_id, product_id) |
| 별점은 1 ~ 5 정수, 필수 | rating INT NOT NULL + CHECK (rating BETWEEN 1 AND 5) |
| 후기 글은 선택 | review_content TEXT NULL |
| 아직 수정 안 했으면 비어 있다 | updated_at DATETIME NULL |
shop의 다른 테이블들과 비교해 보면 새로 등장한 제약은 두 개뿐입니다. UNIQUE KEY와 CHECK — 둘 다 요구사항을 지키기 위해 우리가 직접 결정해서 추가한 줄이에요.
④ 작은 사례로 검산합니다
여기까지가 설계입니다. 그런데 설계는 그려놓고 끝내는 게 아니라 검산해야 합니다. 실제 값을 몇 개 넣어보고, 처음 확인한 요구사항대로 허용되거나 막히는지 확인하는 거예요.
우리 shop의 진짜 데이터를 쓰겠습니다. 회원 1번은 김철수, 상품 23번은 데이터 모델링 실전(32,000원, 도서)입니다.
| 넣어보는 사례 | 예상 결과 | 지켜주는 규칙 |
|---|---|---|
| 다크 초콜릿(상품 20번)에 후기가 0개인 상태 | 허용 | 상품 → 후기는 0..N |
| 김철수(1번)가 상품 23번에 별점 5점 | 허용 | 정상 회원 · 정상 상품 · 별점 범위 안 |
| 이영희(2번)도 상품 23번에 별점 4점 | 허용 | 회원·상품 조합이 다르다 |
| 김철수(1번)가 상품 23번에 두 번째 후기 | 차단 | UNIQUE (member_id, product_id) |
| 별점 6점 | 차단 | CHECK — 별점 1 ~ 5 |
| 후기 글 없이 별점만 입력 | 허용 | review_content 는 선택 |
| 회원 99번이 작성 (없는 회원) | 차단 | 외래키 → members |
| 상품 500번에 작성 (없는 상품) | 차단 | 외래키 → products |
여덟 줄을 하나씩 짚어보면, 우리가 표로 적어둔 요구사항이 하나도 빠짐없이 구조 어딘가에서 지켜지고 있다는 걸 확인할 수 있습니다. 반대로 어떤 요구사항이 어느 줄에서도 지켜지지 않는다면 — 그건 설계에 구멍이 있다는 신호예요.
그림을 예쁘게 고칠 것이 아니라, 빠진 업무 규칙이나 제약을 찾아야 합니다.
검산에서 걸러지지 않는 것도 있습니다. 예를 들어 “구매하지 않은 상품에도 후기를 쓸 수 있다” 는 지금 구조에서 그냥 통과해요. 이건 버그가 아니라 — 우리가 ①에서 “이번 버전에서는 조건으로 삼지 않는다”고 결정했기 때문입니다. 확인한 규칙과 미정 규칙을 분리해 뒀기 때문에, 통과한 이유를 설명할 수 있는 거죠.
⑤ AI는 설계자가 아니라 검토자로 씁니다
완성한 설계는 AI에게 검토를 맡길 수 있습니다. 다만 맡기는 방식이 결과를 완전히 바꿔요.
말하지 않은 규칙중복 작성 · 별점 범위를 AI가 임의로 가정한다
그럴듯한 결과맞는지 틀린지 판단할 기준이 나에게 없다
남는 것내가 이해하지 못한 구조 하나
기준이 있다내가 확인한 요구사항이 채점표가 된다
반례를 받는다내 구조가 막지 못하는 사례를 찾아준다
남는 것근거를 아는 구조 + 검토받은 목록
새 ERD를 다시 만들어달라고 하지 마세요. 확인한 요구사항과 내 1차 설계를 함께 주고 — 반례를 찾게 하는 겁니다.
이렇게 요청하면 됩니다.
아래 요구사항과 제 설계를 비교해 주세요.
새 ERD를 다시 만들지 말고 다음 네 가지를 구분해서 알려주세요.
1. 제가 임의로 가정한 내용
2. 현재 구조가 막지 못하는 잘못된 사례
3. 빠진 제약
4. 최소 수정안AI가 찾아준 제안을 그대로 반영하는 것이 아니라 — 실제 업무 규칙과 비교해 받아들일지 말지 정하는 것까지가 설계입니다. AI는 반례를 잘 찾지만, 우리 서비스의 업무 규칙은 모르니까요.
섹션 4가 쌓아 올린 것
오늘 한 바퀴 돌아본 과정은 사실 4-1부터 하나씩 배운 것들입니다.
| 강의 | 무엇을 쌓았나 | 오늘 어디서 썼나 |
|---|---|---|
| 4-1 관계의 구조 | 무엇을 기록할지 · 한 행은 무엇인지 | ② 한 행 = 후기 한 건 |
| 4-2 부모 · 자식 | 외래키를 어느 쪽에 둘지 | ③ 후기가 회원·상품 번호를 가진다 |
| 4-3 관계의 종류 | 1:1 · 1:N · N:M 을 문장으로 가려내기 | ③ 양쪽 모두 1:N |
| 4-4 ERD 읽는 법 | 최소·최대 개수를 기호로 읽기 | ③ 0..N 과 1..1 |
| 4-5 정규화 | 사실의 주인을 한 곳에 두기 | ② 이름을 복사하지 않는다 |
| 4-6 실무 ERD 분석 | 관계선이 보여주지 않는 규칙 찾기 | ③ 중복 금지를 따로 적는다 |
| 4-7 종합 실습 | 요구사항 → 구조 → 검산 | 오늘 전부 |
좋은 모델은 그럴듯한 그림이 아니라 — 확인한 업무 규칙을 구조로 옮기고 실제 사례로 검산한 결과입니다.
한눈에 정리
| 단계 | 하는 일 | 후기 설계에서는 |
|---|---|---|
| ① 요구사항 질문 | 빈칸을 질문으로 채우고 미정과 분리 | 중복 작성 · 별점 범위 · 필수 여부 확인 / 삭제 정책 보류 |
| ② 한 행의 의미 | 새 테이블과 한 행의 뜻을 확정 | 한 회원이 한 상품에 작성한 후기 한 건 |
| ③ 키 · 관계 · 제약 | 업무 문장을 PK · FK · 제약으로 옮김 | PK review_id / FK 둘 / UNIQUE + 별점 1~5 |
| ④ 검산 | 실제 값을 넣어 허용·차단을 확인 | 김철수 — 상품 23번 사례 여덟 줄 |
| ⑤ AI 검토 | 설계가 아니라 반례·누락·최소 수정안 요청 | 요구사항 + 1차 설계를 함께 전달 |
관계선은 “존재하는 부모를 가리키는가”까지만 지켜줍니다. 중복 금지와 값의 범위처럼 선 밖의 규칙은 우리가 따로 적어야 해요. 그리고 그 모든 결정은 — 작은 사례를 넣어 검산했을 때 처음 확인한 요구사항과 같은 결과가 나와야 비로소 완성입니다.
여기까지가 데이터의 길을 만드는 일이었습니다. 이제 그 길에서 원하는 데이터를 실제로 꺼낼 차례예요. 모델링에서 만든 구조가 곧 SQL을 작성하는 지도가 됩니다.