┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1. [RDB의 본질] : 데이터 간의 '관계성(Relationship)'을 정의하여 정리하기로 한 약속 │
│ 2. [정규화의 질문] : "이 컬럼은 누구의 속성인가?" ➔ 자기 엔티티 테이블에 속성을 올바르게 격리 │
│ 3. [조립의 도구] : 테이블 간 관계는 'FK'로 표현하고, 필요한 화면/API 데이터는 'JOIN'으로 조립│
│ 4. [비정규화 목적] : 과도한 JOIN으로 인한 조회 Disk I/O 부하를 줄이기 위해 의도적으로 중복 허용│
└─────────────────────────────────────────────────────────────────────────────────────────────┘필자 역시 정규화의 학술적 정의(1NF~5NF)를 무작정 달달 외우기보다는 데이터 효율, 중복 제거, 관계 중시 같은 직관적인 키워드로 기억하다 보니, 누군가에게 설명할 때도 흔히 "데이터를 효율적으로 쓰기 위한 중복 제거 작업" 정도로 축약해 설명할 때가 많았다.
하지만 이러한 설명은 데이터베이스에 대한 충분한 배경지식이 이미 갖춰진 사람에게만 납득될 뿐, 실제 데이터 모델링의 본질을 관통하는 온전한 설명이 되기는 어렵다.
1NF부터 5NF까지 각각의 단계에 딱 들어맞는 교과서적이고 인위적인 테이블 예시를 억지로 만들다 보면, 실제 실무 도메인과 괴리가 생겨 오히려 설명의 난해함과 오해를 초래하기 십상이다.
🌟 정규화의 본질적 정의:
따라서 실무에서는 정규화의 수식이나 학술적 정의에 얽매이기보다, "이 컬럼은 누구의 속성인가?", "이 관계는 어떤 외래키(FK)로 맺어지고 어떻게 JOIN으로 복원되는가?"라는 관점으로 테이블을 바라볼 때 가장 명쾌하게 데이터 구조를 설계할 수 있다.
ORDER_RAW)의 문제점쇼핑몰에서 주문 시스템을 처음 개발하며 단일 테이블에 모든 데이터를 몰아넣었다고 가정하자.
[ 구조적 문제점 ]
order_id = 1001
├─ 회원 정보 (user_id=U01, user_name=김철수, user_phone=010...)
└─ 여러 상품 정보 (P01=키보드 수량 1개, P02=마우스 수량 2개)
➔ 한 행(Row)에 주문 + 회원 + 다중 상품 정보가 모두 섞여 있어 검색/수정/삭제가 불가능함💡 핵심 규칙: 하나의 컬럼에는 더 이상 쪼갤 수 없는 단 하나의 값(원자값)만 저장한다.
➔ product_ids = "P01,P02", product_names = "키보드,마우스" 같은 다중값/배열 컬럼을 행(Row) 단위로 분리한다.
분리 후 테이블 (ORDER_ITEM)
PRIMARY KEY (order_id, product_id) 복합키 구성.product_name은 사실 order_id와는 무관하며 오직 product_id에 의해서만 결정된다 (product_id \rightarrow product_name).💡 핵심 규칙: 복합 PK를 사용하는 테이블에서, PK의 일부분에만 종속되는 속성을 별도 테이블로 분리한다 (완전 함수 종속화).
➔ (order_id, product_id) 중 product_name과 price는 product_id만 알면 되므로 PRODUCT 테이블로 분리한다.
분리 후 테이블 (2NF 달성)
PRODUCT (상품 마스터 테이블)ORDER_ITEM (주문 상세 매핑 테이블)SELECT
oi.order_id,
oi.product_id,
p.product_name,
p.price,
oi.quantity
FROM order_item oi
JOIN product p
ON oi.product_id = p.product_id;🎯 실무 포인트: 2NF는 복합 PK를 사용하는 N:M 상세/매핑 테이블(
ORDER_ITEM,USER_ROLE,STUDENT_COURSE) 설계 시 필수적으로 점검하는 단계이다.
💡 핵심 규칙 (실무 최다 발생): PK가 아닌 일반 속성이 다른 일반 속성을 결정(X \rightarrow Y \rightarrow Z)하면 분리한다.
➔ 주문 테이블에서 order_id -> user_id -> (user_name, user_phone) 관계가 존재하므로, 회원 정보를 USER 테이블로 분리한다.
ORDERS_TEMP - 3NF 위반)USER (회원 마스터 테이블)ORDERS (주문 기본 테이블)SELECT
o.order_id,
u.user_name,
u.user_phone,
o.order_date
FROM orders o
JOIN user u
ON o.user_id = u.user_id;🎯 실무 효과: 회원의 전화번호나 이름이 변경되어도 수천 건의 주문 데이터를 수정할 필요 없이
USER테이블 단 1곳만UPDATE하면 되므로 데이터 갱신 이상을 완벽히 차단한다.
1NF부터 3NF까지 완료된 구조는 모든 엔티티가 자기 고유의 속성만을 소유하며, 관계는 외래키(FK)로 연결된다.
USER (회원 엔티티)
--------------------
PK user_id
user_name
user_phone
│
│ 1 : N (FK user_id)
▼
ORDERS (주문 엔티티)
--------------------
PK order_id
FK user_id
order_date
│
│ 1 : N (FK order_id)
▼
ORDER_ITEM (주문상품 매핑)
--------------------
PK,FK order_id
PK,FK product_id
quantity
│
│ N : 1 (FK product_id)
▼
PRODUCT (상품 엔티티)
--------------------
PK product_id
product_name
priceSELECT
o.order_id,
o.order_date,
u.user_id,
u.user_name,
p.product_id,
p.product_name,
p.price,
oi.quantity
FROM orders o
JOIN user u
ON o.user_id = u.user_id
JOIN order_item oi
ON o.order_id = oi.order_id
JOIN product p
ON oi.product_id = p.product_id;🔑 정규화와 조회의 핵심 진리:
JOIN으로 다시 합친다."💡 핵심 규칙: 모든 결정자(Determinant, 다른 속성을 결정하는 컬럼)는 반드시 후보키(Candidate Key)여야 한다.
COURSE_TEACHER - BCNF 위반)업무 규칙:
(student_id, course_id) -> teacher_id (학생과 과목을 알면 담당 교수가 결정됨)
teacher_id -> course_id (교수는 오직 1개 과목만 전담함)
문제점: teacher_id가 결정자이지만 기본키/후보키가 아님.
TEACHER_COURSE (교수 담당 과목 테이블)STUDENT_TEACHER (학생 지도교수 매핑 테이블)💡 핵심 규칙: 하나의 엔티티에 서로 독립적인 다중값(N:M 속성)이 2개 이상 존재하면 분리한다.
DEVELOPER_INFO - 카테시안 곱 발생)개발자 U01이 다룰 수 있는 언어가 2개(Java, Python)이고, 보유 자격증이 2개(정보처리기사, SQLD)일 때 단일 테이블에 넣으면 2 \times 2 = 4개의 불필요한 중복 행이 생성된다.
DEVELOPER_LANGUAGE (개발자 보유 언어)DEVELOPER_CERTIFICATE (개발자 보유 자격증)🎯 실무 형태:
USER_ROLE,PRODUCT_CATEGORY,EMPLOYEE_SKILL등 독립적인 N:M 관계를 각각의 매핑 테이블로 분리하는 패턴이 4NF에 해당한다.
💡 핵심 규칙: 3개 이상의 엔티티가 N:M:K 복합 관계를 가질 때, 더 작은 2개짜리 관계로 무손실 분해할 수 있다면 분리하고 JOIN으로 복원한다.
SUPPLY - 거대 복합 관계 테이블)SUPPLIER_PRODUCTSUPPLIER_REGIONPRODUCT_REGIONSELECT
sp.supplier_id,
sp.product_id,
sr.region_id
FROM supplier_product sp
JOIN supplier_region sr
ON sp.supplier_id = sr.supplier_id
JOIN product_region pr
ON sp.product_id = pr.product_id
AND sr.region_id = pr.region_id;[ ORDER 테이블을 검토할 때 ]
├─ order_id ──> 주문 고유 식별자 (PK) ➔ ORDER 속성 O
├─ user_id ──> 주문을 한 회원 연결 (FK) ➔ ORDER 관계 O
├─ order_date ──> 주문 발생 시각 ➔ ORDER 속성 O
│
├─ user_name ──> "주문 테이블이 왜 회원 이름을 소유하는가?" ➔ USER 테이블로 이동 (3NF)
├─ user_phone ──> "주문 테이블이 왜 전화번호를 소유하는가?" ➔ USER 테이블로 이동 (3NF)
├─ product_name ──> "주문 테이블이 왜 상품명을 소유하는가?" ➔ PRODUCT 테이블로 이동 (2NF)
└─ category_name ──> "주문 테이블이 왜 카테고리를 소유하는가?" ➔ CATEGORY 테이블로 이동 (3NF)정규화는 데이터 무결성과 CUD(생성·수정·삭제) 안정성을 극대화하지만, 대규모 트래픽 환경에서는 치명적인 조회 부하를 낳는다.
JOIN해야 함 ➔ Disk I/O 폭증, 버퍼 풀 고갈, CPU 부하로 인한 슬로우 쿼리.[ 비정규화 적용 기준 ]
1. 읽기(Read) 비중이 90% 이상이고 응답 속도 최적화가 필수적인 테이블
2. 인덱스 튜닝 및 Redis 캐싱으로도 목표 TPS 달성이 불가능한 구간
3. 대규모 실시간 집계(통계, 카운트, 총액) 연산이 빈번한 구간ORDERS 테이블에 product_name을 복제하여 목록 조회 시 JOIN 제거.POST 테이블에 comment_count, like_count 컬럼을 두고 댓글 작성 시마다 +1 갱신 (O(1) 조회).DAILY_SETTLEMENT_SUMMARY 요약 테이블 생성.💡 데이터 모델링의 황금률:
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.