바이브 코딩 시대의 데이터베이스4-5 정규화 직관 — 왜 나누는가
테크타니 LEARN — 섹션 04 · DB 모델링

정규화 직관 —
왜 테이블을 나누는가

지금까지는 이미 나뉘어 있는 다섯 테이블을 읽었습니다. 이번에는 방향을 뒤집어 볼게요. 회원 · 주문 · 상품이 한 장의 넓은 표에 전부 들어 있다면 무슨 일이 벌어질까요? 그 표에서 각 정보의 주인을 찾아내는 일이 정규화입니다.

난이도보통소요8분준비물4-4 완료오늘이 값의 주인은 누구인가

이번 섹션의 여정

4-3 관계의 종류

완료 ✓

4-4 ERD 읽는 법

완료 ✓

4-5 정규화 직관

지금 여기

4-6 ~ 4-7

실무 ERD · 종합 실습

만약 테이블이 나뉘어 있지 않았다면

4-1에서 “거대한 표 하나로 만들면 안 될까요” 라는 질문을 잠깐 던지고 지나갔습니다. 오늘은 그 표를 실제로 만들어 놓고 하나씩 뜯어보겠습니다.

우리 shop의 값을 그대로 쓰되, 다섯 테이블을 한 장으로 합쳤다고 상상해 볼게요.

비교용 가상 표 · 실제 shop 구조가 아닙니다

아래 표는 shop 에 존재하는 테이블이 아닙니다. 실제 값을 한 장에 합쳤다고 가정한 비교용 표예요. 진짜 shop 은 이미 다섯 테이블로 나뉘어 있습니다.

비교용 가상 표 — 넓은 주문표 (주문 1 · 6 · 7 발췌)
order_idordered_atmember_nameproduct_namecategory_namepricequantity
12025-02-01김철수라운드 티셔츠의류19,0001
12025-02-01김철수프리미엄 그래놀라식품12,0002
62025-04-10김철수무선 이어폰전자기기89,0001
72025-04-20강서연라운드 티셔츠의류19,0002
72025-04-20강서연니트의류48,0001
이 방식으로 shop 전체를 한 장에 담으면 주문상세 한 줄이 한 행이 되어 모두 64행이 됩니다

한 행은 주문에 담긴 상품 한 종류입니다. 그래서 주문 1번은 두 줄, 주문 7번도 두 줄이 되죠.

보기에는 꽤 편해 보입니다. 조회할 때 다른 테이블을 찾아갈 필요도 없고요. 그런데 이 표에는 이미 문제가 심어져 있습니다.

한 줄 안에 다섯 가지 사실이 섞여 있습니다

첫 두 줄을 다시 보세요. 김철수2025-02-01은 똑같이 반복되는데, 상품명 · 카테고리 · 가격 · 수량은 서로 다릅니다.

같은 행에 있다고 해서 같은 성격의 값이 아니라는 뜻이에요. 한 줄을 성격별로 갈라 보면 이렇게 됩니다.

한 줄에 섞여 있는 사실들
이 값은누구의 사실인가주인이 될 테이블
회원 이름 · 성별 · 나이 · 가입일회원 한 명members
주문일 · 주문한 회원주문 한 건orders
상품 이름 · 현재 가격상품 한 개products
카테고리 이름카테고리 하나categories
담긴 상품과 수량주문과 상품의 만남order_items
한 줄 안에서 서로 다른 다섯 주인의 사실이 뒤엉켜 있습니다

3-1에서 배운 “이 테이블에서 한 행의 주인공은 무엇이지” 라는 질문을 이 표에 던져 보세요. 답이 하나로 나오지 않습니다. 주문의 표 같기도 하고, 상품의 표 같기도 하고, 회원의 표 같기도 하죠.

⭐ 오늘의 한 문장

정규화는 표를 많이 만드는 기술이 아니라, 같은 사실을 한 곳에서 책임지게 만드는 정리 방법입니다.

값이 반복되는 것 자체는 문제가 아닙니다

여기서 흔한 오해 하나를 먼저 지우고 갑니다. 같은 값이 여러 줄에 보인다고 무조건 잘못된 설계가 아닙니다.

실제 shoporders 테이블에도 member_id = 1은 여섯 번 나옵니다. 김철수 회원의 주문이 6건이니까요. 이건 관계를 저장하기 위한 정상적인 반복입니다.

같은 값이 반복될 때 — 두 가지 경우
어떤 반복인가문제인가
관계를 가리키는 번호가 반복orders 여섯 줄에 member_id = 1× 정상 — 관계를 저장하려면 필요하다
같은 사실을 여러 줄에 복사해 따로 관리넓은 표 열 줄에 "김철수"라는 이름 자체를 반복 저장○ 문제 — 고칠 곳이 열 군데가 된다
반복되는가가 아니라 — 그 사실을 책임지는 주인이 한 명인가를 봅니다

외래키 값이 반복되는 것은 “이 행이 누구에게 속하는지” 를 가리키는 표지입니다. 문제는 사실 그 자체를 여러 곳에 복사해 놓고 각자 관리하는 상태예요.

이제 그 상태에서 평범한 작업 세 가지를 해보겠습니다.

상황 하나 — 이름 하나 바꾸는 데 열다섯 줄

기획팀에서 카테고리 이름을 “의류” 에서 “패션” 으로 바꾸기로 했습니다. 실제 shop이라면 categories 테이블의 한 줄만 고치면 끝이에요.

그런데 넓은 표에서는 의류 상품이 담긴 주문상세 줄을 전부 찾아야 합니다.

넓은 표에서 '의류'라는 글자가 적혀 있는 줄
상품몇 개 주문에 담겼나
라운드 티셔츠4
패딩 점퍼4
니트3
카디건3
면 셔츠1
합계15
같은 카테고리 이름을 열다섯 줄에서 고쳐야 합니다

열다섯 줄을 고치는 것 자체보다 무서운 건 한 줄을 놓쳤을 때 입니다. 열네 줄은 “패션”, 한 줄은 “의류”가 되죠. 그러면 같은 카테고리인데 이름이 두 개인 상태가 됩니다. DB는 그 한 줄이 틀렸다는 걸 알 방법이 없어요 — 어느 쪽이 진짜인지 판단할 근거가 표 안에 없으니까요.

상품 쪽도 똑같습니다. 라운드 티셔츠 가격이 19,000원에서 21,000원으로 오르면 네 줄을 모두 찾아 고쳐야 합니다.

여기서 나오는 좋은 질문

“가격을 고치면 지난 주문의 금액까지 바뀌는 것 아닌가요?” — 아주 좋은 질문이고, 실무에서는 주문 당시 가격을 따로 남기는 설계를 씁니다. 다만 그건 가격 이력 이라는 별도 주제예요. 오늘은 사실의 주인을 찾는 감각에 집중하겠습니다.

상황 둘 — 아직 팔리지 않은 것은 적을 자리가 없습니다

이번에는 새 데이터를 넣어 봅니다. 넓은 표의 한 행은 주문에 담긴 상품 한 종류였죠. 그래서 주문이 없으면 행 자체가 생기지 않습니다.

넓은 표에 넣을 수 없는 실제 shop 데이터
넣고 싶은 것shop 의 실제 개수넓은 표에서는
아직 주문이 없는 회원4명 (17 ~ 20번, 백서윤 포함)넣을 행이 없다
아직 한 번도 팔리지 않은 상품10개 (슬림핏 청바지 · 다크 초콜릿 등)넣을 행이 없다
상품이 아직 없는 새 카테고리지금은 없지만 언제든 생긴다넣을 행이 없다
실제 shop 의 상품 30개 중 주문상세에 등장하는 것은 20개뿐입니다 — 나머지 10개는 넓은 표에 아예 나타나지 못합니다

가입만 하고 아직 주문하지 않은 백서윤 회원은 존재하지 않는 회원이 됩니다. 창고에 쌓여 있는 다크 초콜릿도 없는 상품이 되고요.

억지로 넣을 방법이 없는 건 아닙니다. 주문번호 · 상품명 · 수량을 비워둔 유령 행을 하나 만들면 되니까요. 하지만 그 순간 “한 행은 주문에 담긴 상품 한 종류” 라는 약속이 깨집니다. 이후 모든 집계가 그 유령 행을 걸러내는 조건을 달고 다녀야 해요.

다시 3-1의 질문

행을 넣기 위해 한 행의 의미를 흐려야 한다면 — 그 표는 서로 다른 사실을 너무 많이 안고 있는 것입니다.

상황 셋 — 주문을 지웠더니 상품이 사라졌습니다

마지막은 삭제입니다. 주문 12번을 취소해서 관련 줄을 지운다고 해봅시다. 실제 shop의 주문 12번에는 두 상품이 들어 있어요.

shop DB — 주문 12번의 실제 주문상세
order_idproduct_idproduct_namepricequantity
129무선 이어폰89,0001
1212USB-C 케이블8,0003
무선 이어폰은 다른 주문에도 담겨 있지만 — USB-C 케이블은 주문 12번이 유일합니다

무선 이어폰은 주문 3 · 6 · 12 · 19 · 22 · 27번에 담겨 있어서, 12번 줄이 사라져도 다른 줄에 이름과 가격이 남습니다. 그런데 USB-C 케이블은 40건의 주문 중 12번에만 들어 있어요.

넓은 표에서 그 줄을 지우는 순간 “USB-C 케이블이라는 상품이 8,000원에 팔리고 있다” 는 사실까지 함께 지워집니다. 우리는 주문 하나를 취소했을 뿐인데 상품 목록에서 상품이 하나 사라진 겁니다.

세 상황의 공통점

수정 · 삽입 · 삭제는 어느 서비스에서나 매일 일어나는 평범한 작업입니다. 그런 평범한 작업이 관계없는 다른 정보까지 흔든다면 — 구조를 의심해야 합니다.

해결은 하나 — 사실을 주인에게 돌려보내기

문제의 원인이 “한 표가 다섯 주인의 사실을 떠안고 있다” 였으니, 해결은 단순합니다. 각 사실을 자기 주인에게 돌려보내는 것.

넓은 표 한 장 → 다섯 테이블
테이블한 행이 뜻하는 것책임지는 사실
members회원 한 명이름 · 성별 · 나이 · 가입일
orders주문 한 건주문일 · 주문한 회원 번호
products상품 한 개상품 이름 · 현재 가격 · 카테고리 번호
categories카테고리 하나카테고리 이름
order_items주문에 담긴 상품 한 종류수량
테이블이 다섯 개가 된 게 아니라 — 주인이 다섯이었던 것을 이제야 인정한 것입니다

이렇게 나누면 아까의 세 상황이 전부 조용해집니다.

같은 작업, 나누기 전과 나눈 뒤

넓은 표 한 장이라면
  • 카테고리 이름 변경열다섯 줄을 찾아 고치고, 하나만 놓쳐도 이름이 갈라진다

  • 주문 없는 회원 등록넣을 행이 없어 유령 행을 만들어야 한다

  • 주문 12번 취소USB-C 케이블이라는 상품 자체가 사라진다

다섯 테이블로 나누면 ✓
  • 카테고리 이름 변경categories 의 한 줄만 고치면 끝난다

  • 주문 없는 회원 등록members 에 한 줄 넣으면 끝난다

  • 주문 12번 취소order_items · orders 만 지워지고 products 는 그대로

테이블을 나눈 진짜 이유는 저장 공간을 아끼는 것도, 표를 예쁘게 만드는 것도 아닙니다 — 고칠 곳을 한 군데로 만들기 위해서입니다.

4-2 · 4-4 와 이어 보면

나뉜 테이블을 다시 이어주는 것이 외래키이고, 그 연결을 그림으로 읽는 방법이 ERD 였습니다. 정규화는 나누는 쪽, 외래키는 잇는 쪽 — 둘은 한 세트로 움직입니다.

수량의 주인은 주문과 상품의 만남입니다

회원 · 주문 · 상품 · 카테고리는 주인을 찾기 쉬웠습니다. 그런데 하나가 애매하게 남습니다. 수량은 누구의 사실일까요?

먼저 상품의 사실인지 확인해 봅니다. 프리미엄 그래놀라의 수량이 정해져 있다면 products 테이블에 두면 되겠죠.

shop DB — 프리미엄 그래놀라(product_id 16)가 담긴 주문
order_idproduct_idquantity
1162
10163
같은 상품인데 주문마다 수량이 다릅니다 — 상품 한 개의 사실이 아닙니다

그럼 주문의 사실일까요? 주문 1번의 수량을 하나로 적으려면 곤란해집니다.

shop DB — 주문 1번의 실제 주문상세
order_idproduct_idproduct_namequantity
11라운드 티셔츠1
116프리미엄 그래놀라2
주문 1번의 수량은 1일까요 2일까요 — 어느 상품의 수량인지 빠져 있습니다

수량은 상품만으로도, 주문만으로도 정해지지 않습니다. 주문 1번상품 16번이 함께 지목되어야 비로소 2라는 값이 정해져요.

⭐ 수량의 주인

수량은 특정 주문과 특정 상품이 만났을 때 비로소 생기는 사실입니다. 그래서 주문도 상품도 아닌 — 둘이 만나는 자리, order_items 에 둡니다.

order_items의 기본키가 (order_id, product_id) 복합키인 것도 같은 이유입니다. 이 행을 특정하려면 두 번호가 모두 필요하니까요. 4-3에서 본 quantity = 2“그래놀라는 언제나 2개” 가 아니라 “주문 1번에 그래놀라가 2개” 였던 이유가 여기 있습니다.

헷갈릴 때 던지는 한 문장

새 컬럼을 어디에 둘지 고민될 때는 이 순서로 물어보세요.

컬럼의 자리를 정하는 질문
① 이 값은 무엇 하나가 정해지면 따라서 정해지는가? — 그 하나의 테이블에 둔다
② 아니면 두 대상이 만나야 비로소 정해지는가? — 둘을 잇는 테이블에 둔다
③ 지금 이 값을 고쳐야 할 때, 고칠 곳이 두 군데 이상인가? — 그렇다면 주인이 잘못 정해진 것이다
나누는 것이 언제나 정답은 아닙니다

테이블을 잘게 나눌수록 조회할 때 이어 붙일 곳이 늘어납니다. 실무에서는 조회 성능 때문에 일부러 합쳐 두는 판단도 합니다. 다만 그건 나누는 이유를 정확히 안 다음에 내리는 선택이에요. 오늘 잡을 것은 그 기준선입니다.

한눈에 정리

넓은 표 한 장에서 벌어지는 일원인나눈 뒤
카테고리 이름을 열다섯 줄에서 고쳐야 한다같은 사실이 여러 줄에 복사돼 있다categories 한 줄만 고친다
주문 없는 회원 · 안 팔린 상품을 못 넣는다한 행이 주문상세를 뜻해서members · products 에 따로 넣는다
주문을 지우면 상품 정보까지 사라진다서로 다른 사실이 한 줄에 묶여 있다각 테이블이 독립적으로 남는다
사실주인테이블
이름 · 성별 · 나이 · 가입일회원 한 명members
주문일 · 주문한 회원주문 한 건orders
상품 이름 · 현재 가격상품 한 개products
카테고리 이름카테고리 하나categories
수량주문과 상품의 만남order_items

값이 반복된다는 이유만으로 표를 나누는 것이 아닙니다. 같은 사실을 여러 줄에서 따로 관리하고 있다면 그 사실을 한 명의 주인에게 돌려보내는 것 — 그게 정규화입니다.

이제 다섯 테이블이 왜 다섯 개인지 알았습니다. 다음 강의에서는 이렇게 나뉜 테이블을 실제로 따라가 볼 차례입니다.

NEXT LESSON4-6 실무 ERD 분석 — 지금 한 행은 무엇인가

경로를 지날 때마다 한 행의 단위가 어떻게 달라지는지 — 그리고 관계선이 막지 못하는 규칙을 봅니다.

GO ▸ 4-6 시작하기