백엔드
데이터베이스
DB 정규화: 이상 현상, 1NF부터 3NF, 반정규화 기준

DB 정규화: 이상 현상, 1NF부터 3NF, 반정규화 기준

들어가며

고객이 전화번호를 바꿨다고 해서 고쳤는데, 지난 주문 화면에는 여전히 옛 번호가 보이는 경우가 있습니다. 같은 값을 여러 행에 복사해 두고 그중 일부만 고쳤기 때문입니다.

이번 글에서는 이런 어긋남을 막는 정규화를 정리합니다. 중복이 만드는 세 가지 이상, 1NF부터 3NF까지의 규칙, 일부러 중복을 두는 반정규화의 기준을 다룹니다.


1. 중복이 만드는 세 가지 이상

정규화는 중복을 줄이도록 테이블을 나눠, 한 사실을 한 곳에만 두는 설계입니다. 주문 테이블에 고객 이름과 전화번호를 함께 넣으면 처음에는 조인 없이 조회할 수 있어 편합니다. 문제는 값을 바꾸거나 지울 때 드러납니다.

  • 갱신 이상: 번호를 바꾸려면 그 고객의 주문을 모두 고쳐야 하고, 하나라도 빠지면 값이 갈립니다.
  • 삽입 이상: 주문 테이블에만 고객 정보가 있으면 아직 주문하지 않은 고객을 저장할 곳이 없습니다.
  • 삭제 이상: 고객의 마지막 주문을 지우면 고객 정보까지 함께 사라집니다.

정규화는 친구 번호를 주소록 한 곳에만 적어 두는 일이에요.

이상주문에 복사고객 분리
갱신 이상번호 변경3행 수정1행 수정
삽입 이상주문 없는 고객저장 불가저장 가능
삭제 이상마지막 주문 삭제고객도 삭제고객 유지
고객 정보를 분리하면 세 이상이 모두 사라진다. 대신 주문과 고객을 함께 볼 때 조인이 필요하다.
-- 나쁨: 주문마다 전화번호를 복사
-- orders(id, customer_name, phone, item)
UPDATE orders SET phone = '010-2222' WHERE id = 7;
-- 같은 고객의 다른 주문 2건은 옛 번호로 남는다
 
-- 좋음: 고객 정보는 한 곳에
-- customers(id, name, phone)
-- orders(id, customer_id, item)
UPDATE customers SET phone = '010-2222' WHERE id = 3;

2. 1NF, 2NF, 3NF

정규형은 단계별 규칙입니다. 실무에서 기본으로 삼는 3NF까지는 규칙 세 개로 정리됩니다.

1NF는 한 칸에 값 하나만 둡니다. 태그1,태그2 처럼 한 칸에 목록을 넣으면 특정 태그를 찾거나 하나만 지우기가 어려워지므로, 행으로 나눕니다.

2NF는 복합 키의 일부에만 의존하는 컬럼을 뺍니다. (주문 ID, 상품 ID) 가 키인 주문 항목 테이블에 상품명을 두면, 상품명은 상품 ID만으로 정해지므로 상품 테이블로 옮깁니다.

3NF는 키가 아닌 컬럼에 의존하는 컬럼을 뺍니다. 주문에 고객 ID와 고객 전화번호를 함께 두면, 전화번호는 주문이 아니라 고객 ID에 따라 정해지므로 고객 테이블로 옮깁니다. 이렇게 나누면 아래와 같은 구조가 됩니다.

각 표는 자기 사실만 적은 장부이고, 다른 장부는 번호로 가리켜요.

🧾주문 항목order_items📋주문orders📦상품상품명·가격👤고객이름·전화번호주문 ID상품 ID고객 ID
화살표는 외래 키가 가리키는 방향이다. 전화번호와 상품명을 고칠 곳은 한 곳으로 줄지만, 주문 화면을 만들려면 표를 잇는 조인이 늘어난다.
CREATE TABLE customers (
  id    BIGINT PRIMARY KEY,
  name  TEXT NOT NULL,
  phone TEXT
);
 
CREATE TABLE orders (
  id          BIGINT PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers (id)
);
 
SELECT o.id, c.name, c.phone
FROM orders o
JOIN customers c ON c.id = o.customer_id;

쓰기 이상은 사라지지만 읽을 때 조인이 필요합니다. 이것이 정규화의 대가입니다.

3. 반정규화: 일부러 복사할 때

반정규화는 읽기 속도를 위해 일부러 중복을 두는 설계입니다. 조회가 많은 목록에서 조인과 집계가 실제 병목으로 측정됐다면 고객 이름을 주문에 복사하거나, 게시글에 댓글 수 컬럼을 둘 수 있습니다.

대신 원본과 맞추는 책임이 생깁니다. 같은 트랜잭션에서 함께 고치면 어긋나지 않지만 쓰기가 무거워지고, 배치나 이벤트로 나중에 맞추면 그동안 옛 값이 보입니다. 5분마다 도는 배치라면 다음과 같습니다.

복사본은 게시판에 붙인 공지 사본이에요. 원본이 바뀌어도 다시 붙이기 전까지는 옛 내용이에요.

원본옛 이름새 이름복사본옛 이름어긋남새 이름이름 변경배치 반영05101520분
복사본을 읽으면 조인을 줄일 수 있지만 배치가 돌기 전까지 옛 값이 보인다. 배치 주기를 줄일수록 지연은 줄고 쓰기 부담은 늘어난다.

그래서 복사하기 전에 얼마나 늦어도 되는지를 먼저 정합니다. 댓글 수가 몇 분 늦는 것은 대개 괜찮지만, 잔액이나 재고가 늦으면 곤란합니다. 인덱스 추가나 쿼리 수정으로 풀리는 문제라면 복사본을 만들 이유도 없습니다.

헷갈리기 쉬운 점

  • 정규화 단계는 높을수록 좋다는 오해. 테이블이 잘게 나뉠수록 조인이 늘어납니다. 보통 3NF를 기본으로 두고, 측정한 병목에서만 반정규화합니다.
  • 값이 두 곳에 있으면 모두 위반이라는 오해. 상품의 현재 가격과 주문 시점의 가격은 다른 사실입니다. 주문에 가격을 저장하는 것은 중복이 아니라 기록입니다.
  • 조인이 있으면 무조건 느리다는 오해. 인덱스를 탄 키로 적은 행을 잇는 조인은 빠릅니다. 추측 대신 실행 계획으로 확인합니다.

면접에서 자주 받는 질문

주문에 당시 가격을 복사하는 것은 정규화 위반인가요?

위반이 아닙니다. 현재 가격과 주문 시점의 가격은 서로 다른 사실입니다. 상품 테이블만 참조하면 가격이 바뀔 때 과거 주문 금액까지 바뀌어 버립니다.

반정규화한 값은 어떻게 원본과 맞추나요?

같은 트랜잭션에서 함께 고치거나, 트리거, 배치, 이벤트로 나중에 맞춥니다. 나중에 맞추면 그동안 옛 값이 보이므로 허용할 지연 시간을 먼저 정합니다. 어긋났을 때 원본에서 다시 계산하는 방법도 준비해 둡니다.

3NF와 BCNF는 무엇이 다른가요?

BCNF는 다른 컬럼을 결정하는 컬럼이 모두 후보 키여야 한다는 더 엄격한 규칙입니다. 대부분의 테이블은 3NF면 BCNF도 만족합니다. 다만 BCNF로 나누면 기존 제약을 한 테이블에서 강제하지 못할 수 있어, 실무에서는 3NF에서 멈추기도 합니다.

정리

  • 정규화는 한 사실을 한 곳에 두어 갱신, 삽입, 삭제 이상을 막습니다.
  • 1NF는 한 칸에 값 하나, 2NF는 키 전체에 의존, 3NF는 키에만 직접 의존을 요구합니다.
  • 반정규화는 측정한 병목에만 쓰고, 원본과 맞추는 방법과 허용 지연을 함께 정합니다.

한 사실은 한 곳에 두고, 빠르게 읽어야 할 곳에만 이유를 갖고 복사합니다.