데이터베이스 정규화를 이론 과목으로 기억하는 분이 많은데, 실무에서 정규화는 시험이 아니라 사고 예방입니다. 1차·2차·3차 정규형이 각각 어떤 사고를 막는지 게시판과 쇼핑몰 예제로 하나씩 확인하고, 마지막에는 언제 일부러 깨는지(반정규화)까지 다룹니다.
사고 장면부터 보겠습니다. 회원 주소가 회원 테이블, 주문 테이블, 배송 테이블 세 곳에 저장되어 있는 DB가 있습니다. 회원이 이사해서 한 곳만 고쳤다면, 이제 같은 회원의 주소가 세 곳에서 두 가지 값으로 존재합니다. 어느 쪽이 진짜일까요? 이 질문에 답할 수 없는 상태가 갱신 이상이고, 정규화는 이 사고를 구조적으로 막는 방법입니다.
한눈에: 1NF·2NF·3NF가 각각 막는 사고
세 정규형의 차이를 표로 먼저 잡아 두면 아래 예제들을 빨리 습득할 수 있습니다.
| 정규형 | 잡아내는 구조 | 대표 증상 |
|---|---|---|
| 1NF (제1정규형) | 한 칸에 여러 값 | 콤마로 이어 넣은 태그를 검색·조인할 수 없음 |
| 2NF (제2정규형) | 복합키 일부에만 종속된 컬럼 | 주문 상세마다 상품명이 복사되어 수정이 대규모 작업이 됨 |
| 3NF (제3정규형) | 키 아닌 컬럼에 종속된 컬럼 | 우편번호를 고치면 도시도 같이 고쳐야 함 |
이제 하나씩, 위반 DDL과 고친 DDL을 나란히 보면서 갑니다.
제1정규형(1NF): 콤마 컬럼 예시로 이해하기
가장 자주 보는 위반부터입니다. 게시글에 태그를 붙이려는데 테이블을 늘리기 싫어서 이렇게 만든 경우입니다.
-- 1NF 위반: 한 컬럼에 여러 값
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
tags VARCHAR(255) NULL COMMENT '태그: mysql,erd,정규화 처럼 콤마로'
);
처음엔 잘 돌아갑니다. 문제는 "erd 태그가 붙은 글 목록"이 필요해지는 순간부터입니다. LIKE '%erd%'로 검색하면 'erd-tool' 태그까지 걸리고, 인덱스는 못 타고, 태그 이름을 하나 바꾸려면 문자열을 전부 파싱해서 고쳐야 합니다. 한 칸에 값이 여러 개 들어가는 순간 DB가 그 값들을 데이터로 다루지 못하게 되는 겁니다.
1NF는 "모든 칸에 값이 하나만"이라는 규칙이고, 고치는 방법은 값마다 행을 갖게 하는 것입니다.
-- 1NF 충족: 태그를 행으로
CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE COMMENT '태그명'
);
CREATE TABLE post_tags (
post_id BIGINT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (post_id, tag_id),
CONSTRAINT fk_pt_post FOREIGN KEY (post_id) REFERENCES posts (id),
CONSTRAINT fk_pt_tag FOREIGN KEY (tag_id) REFERENCES tags (id)
) COMMENT='게시글-태그';
확인 포인트는 하나입니다. 콤마·슬래시·공백으로 값을 이어 넣은 컬럼이 있는가. 있다면 나중에 검색이 필요해지고 나서 고치는 것보다, 지금 분리해 두는 쪽이 수정 비용이 훨씬 적습니다.
제2정규형(2NF): 복합키의 절반에만 종속된 컬럼
2NF는 복합 기본키를 쓰는 테이블에서만 문제가 됩니다. 쇼핑몰의 주문 상세 테이블로 살펴보겠습니다.
-- 2NF 위반: product_name이 복합키의 일부(product_id)에만 종속
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(200) NOT NULL COMMENT '상품명 (문제의 컬럼)',
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
이 테이블의 기본키는 (order_id, product_id) 복합키입니다. 그런데 product_name은 주문과 무관하게 product_id만 알면 정해지는 값입니다. 결과는 같은 상품명이 주문 상세 수만큼 복사되는 것이고, 상품명을 바꾸는 날 수만 행을 UPDATE 하거나, 일부만 고쳐져 같은 상품이 두 가지 이름으로 남게 됩니다.
부분 함수 종속이란
방금 상황을 부르는 이름이 부분 함수 종속입니다. 어렵게 들리지만 뜻은 그대로입니다. 복합키의 일부만으로 정해지는 컬럼이 그 테이블에 들어와 있는 상태. product_name은 키의 절반(product_id)에만 종속되어 있으니 부분 종속이고, 2NF는 이런 컬럼을 자기 키가 있는 테이블로 옮기라는 규칙입니다.
-- 2NF 충족: 상품 정보는 products로
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL COMMENT '상품명'
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id),
CONSTRAINT fk_oi_product FOREIGN KEY (product_id) REFERENCES products (id)
);
확인 포인트: 복합키 테이블에서 "이 컬럼, 키 전체가 아니라 일부만 알아도 정해지나?"를 컬럼마다 물어보면 됩니다. 하나라도 그렇다면 그 컬럼의 자리는 다른 테이블입니다.
제3정규형(3NF): 키가 아닌 컬럼에 종속된 컬럼
3NF는 복합키가 없어도 걸립니다. 회원 테이블에 주소를 넣다가 흔히 생기는 모양입니다.
-- 3NF 위반: city가 키(id)가 아니라 zip_code에 종속
CREATE TABLE members (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
zip_code CHAR(5) NULL COMMENT '우편번호',
city VARCHAR(50) NULL COMMENT '도시 (문제의 컬럼)'
);
city는 회원(id)이 정하는 값이 아니라 zip_code가 정하는 값입니다. 우편번호를 알면 도시는 자동으로 정해지니까요. 이 상태에서는 같은 우편번호의 도시명이 회원 수만큼 복사되고, 행정구역 개편으로 도시명이 바뀌면 회원 테이블을 수만 행 고치게 됩니다. 회원 정보를 고치다가 주소 체계가 어긋나는, 도입부의 그 사고입니다.
이행적 종속 예시
이 구조의 이름은 이행적 종속입니다. id가 zip_code를 정하고, zip_code가 city를 정하니, id → zip_code → city로 간접적인 종속이 이어진 상태입니다. 3NF는 이 연결을 끊어서, 키가 아닌 컬럼(zip_code)에 종속된 컬럼(city)을 별도 테이블로 옮기라는 규칙입니다.
-- 3NF 충족: 우편번호 정보는 zip_codes로
CREATE TABLE zip_codes (
zip_code CHAR(5) PRIMARY KEY COMMENT '우편번호',
city VARCHAR(50) NOT NULL COMMENT '도시'
);
CREATE TABLE members (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
zip_code CHAR(5) NULL,
CONSTRAINT fk_members_zip FOREIGN KEY (zip_code) REFERENCES zip_codes (zip_code)
);
확인 포인트: "이 컬럼, 기본키 말고 다른 컬럼만 알아도 정해지나?" 정해진다면 그 다른 컬럼을 키로 갖는 테이블로 옮겨야 합니다.
BCNF부터는 왜 잘 이야기하지 않나
교과서에는 BCNF, 4NF, 5NF가 이어지지만, 실무 대부분은 3NF에서 멈춥니다. BCNF 이상이 3NF와 다른 결과를 내는 경우는 후보키가 여러 개 겹치거나 다중 값 종속이 있는 특수한 구조뿐이라, 일반적인 업무 스키마에서는 3NF를 지키면 자동으로 충족되는 경우가 많습니다. 3NF까지를 기본기로 확실히 하고, 그 위는 해당 구조를 만났을 때 찾아봐도 늦지 않습니다.
반정규화는 언제 하는가: 판단 기준 3가지
여기까지 읽고 "그럼 무조건 쪼개면 되는구나"로 끝나면 절반만 배운 겁니다. 실무에는 정규형을 알면서 일부러 깨는 선택, 반정규화가 있습니다. 문제는 깨는 것 자체가 아니라 근거 없이 깨는 것이라서, 설계 리뷰에서 반정규화를 승인할 때 보는 기준은 세 가지입니다.
- 측정된 병목인가. "조인이 많으면 느려질 것 같아서"는 근거가 아닙니다. 실제 쿼리의 실행 계획과 응답 시간이 문제로 측정된 다음의 이야기입니다. 추측으로 깬 중복은 성능은 그대로인데 사고 위험만 얹는 결과가 되기 쉽습니다.
- 갱신 경로가 한 곳으로 관리되는가. 중복을 만들면 두 값을 맞추는 책임이 애플리케이션으로 넘어옵니다. 그 갱신이 트리거든 배치든 코드 한 곳이든, 단일한 경로로 관리된다는 답이 있어야 합니다.
- 깬 사실이 문서에 남는가. 다음 담당자가 "이거 중복인데 실수인가?" 하고 정리해 버리는 순간 사고가 됩니다. 어떤 컬럼이 왜 중복인지 테이블 명세서에 남기는 것까지가 반정규화입니다.
흔한 사례 두 유형도 구분해 둘 필요가 있습니다. 집계 컬럼(게시글의 댓글 수 같은)은 세면 나오는 값을 성능 때문에 저장하는 전형적인 반정규화라 위의 세 기준을 전부 통과해야 합니다. 반면 스냅샷 컬럼(주문 상세의 주문 시점 단가)은 아예 반정규화가 아닙니다. 상품의 현재 가격과 주문 당시 가격은 다른 의미의 데이터라, 복사가 아니라 그 시점의 사실 기록입니다. 이 구분이 서면 중복처럼 보이는 컬럼이 나왔을 때 판단이 빨라집니다.
정규화한 결과를 눈으로 확인하기
정규화는 테이블을 쪼개는 작업이라, 끝나고 나면 테이블 수가 늘고 관계선이 생깁니다. 이 시점에 구조를 눈으로 확인해 두는 게 좋습니다. 위에서 고친 DDL을 그대로 WorksCove ERD에 붙여넣으면 이렇게 그려집니다.

콤마 컬럼이던 태그가 post_tags 중간 테이블로, 회원 안에 있던 도시가 zip_codes 참조로 바뀐 구조가 관계선으로 보입니다. 관계선 기호가 낯설면 ERD 표기법 총정리에서 확인할 수 있습니다. 다이어그램에서 FK가 관계의 N쪽에 제대로 있는지, 쪼갠 테이블이 의도대로 연결됐는지 확인하는 방법은 ERD 그리는 법의 점검 목록을 그대로 쓰면 되고, DDL을 뽑고 붙여넣는 과정 자체는 SQL to ERD에 정리되어 있습니다.
자주 묻는 질문
정규화는 몇 정규형까지 해야 하나요?
실무 기준은 3NF까지입니다. 1NF에서 3NF까지가 실무 사고의 대부분을 막아 주고, 그 위의 BCNF·4NF는 겹치는 후보키나 다중 값 종속처럼 특수한 구조에서만 3NF와 결과가 달라집니다. 3NF를 기본으로 두고, 성능 때문에 일부러 깨는 반정규화를 예외로 관리하는 게 현실적인 운영 방식입니다.
주문 테이블에 주문 시점 가격을 복사해 두는 건 정규화 위반인가요?
위반이 아니라 올바른 설계입니다. 주문 시점의 가격은 상품의 현재 가격과 다른 의미의 데이터라서, 상품 가격이 바뀌어도 과거 주문 금액은 그대로 남아야 합니다. 같은 값을 중복 저장하는 게 아니라 그 시점의 사실을 기록하는 것이므로, 스냅샷 컬럼은 정규화와 충돌하지 않습니다.
JSON 컬럼에 여러 값을 넣으면 1NF 위반인가요?
그 값으로 검색하거나 조인해야 한다면 위반이 만드는 문제를 그대로 겪게 됩니다. 태그를 JSON 배열로 넣으면 콤마 컬럼과 똑같이 인덱스와 무결성 문제가 생깁니다. 반대로 통째로 저장하고 통째로 읽기만 하는 설정값·로그성 데이터라면 JSON 컬럼이 실용적인 선택일 수 있습니다. 그 값이 관계로 쓰이는지가 판단 기준입니다.
NoSQL 시대에도 정규화가 필요한가요?
관계형 DB를 쓰는 한 필요합니다. NoSQL이 중복을 허용하는 건 정규화가 틀려서가 아니라, 갱신 이상의 관리를 애플리케이션이 맡는 대신 조회 성능을 얻는 다른 방식을 선택한 것입니다. 그 선택의 대가를 이해하려면 오히려 정규화가 막아 주던 사고가 무엇인지 알아야 하니, 어느 쪽을 쓰든 출발점은 같습니다.
마지막으로 요약합니다. 한 칸에 값 하나(1NF), 복합키 전체에 종속(2NF), 키에만 종속(3NF). 이 셋을 기본으로 지키고, 깰 때는 측정·갱신 경로·문서화 세 가지를 확인하고 깬다. 정규화를 이 정도로 잡아 두면, 시험공부가 아니라 내일 설계 리뷰에서 바로 쓰는 기준이 됩니다.