DBGuide

정규화란 무엇인가

📖 약 3🏷 스키마 설계

왜 스키마 설계가 중요한가

나쁜 스키마는 처음엔 잘 동작합니다. 문제는 나중에 옵니다. 데이터 중복, 업데이트 이상, 삭제 이상 — 이 세 가지가 나쁜 설계의 증상이에요.

예를 들어, 주문 테이블에 고객 이름을 직접 저장했다면:

  • 고객이 이름을 바꾸면 모든 주문 행을 수정해야 합니다 (업데이트 이상)
  • 고객의 마지막 주문을 삭제하면 고객 정보도 사라집니다 (삭제 이상)
  • 같은 고객 정보가 수천 개 주문 행에 중복 저장됩니다 (삽입 이상)

**정규화(Normalization)**는 이런 이상 현상을 방지하는 설계 원칙입니다.

1NF: 원자값만 저장

각 셀에는 하나의 값만 들어가야 합니다. 여러 값을 콤마로 이어 붙이거나, 반복 컬럼을 만들면 안 됩니다.

-- 나쁜 예: 한 셀에 여러 값
-- | user_id | phone_numbers        |
-- | 1       | 010-1111, 010-2222   |  ← 나쁨
-- → 특정 번호로 검색, 번호 삭제가 매우 어려워짐

-- 나쁜 예: 반복 컬럼
-- | user_id | phone1     | phone2     |
-- | 1       | 010-1111   | 010-2222   |  ← 나쁨
-- → 번호가 3개면 테이블 구조를 바꿔야 함

-- 좋은 예: 별도 테이블로 분리
CREATE TABLE user_phones (
  id      SERIAL PRIMARY KEY,
  user_id INT NOT NULL REFERENCES users(id),
  phone   VARCHAR(20) NOT NULL
);

2NF: 부분 종속 제거

복합 기본 키가 있을 때, 모든 열은 기본 키 전체에 의존해야 합니다. 기본 키의 일부에만 의존하는 컬럼은 별도 테이블로 빼야 합니다.

-- 나쁜 예: order_items 테이블에서
-- (order_id, product_id)가 기본 키인데
-- product_name은 product_id에만 종속됨
-- → products 테이블로 분리

CREATE TABLE products (
  id   SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  price INT NOT NULL
);

CREATE TABLE order_items (
  order_id   INT REFERENCES orders(id),
  product_id INT REFERENCES products(id),
  quantity   INT NOT NULL,
  unit_price INT NOT NULL,  -- 주문 시점 가격 스냅샷 (의도적 중복)
  PRIMARY KEY (order_id, product_id)
);

팁: unit_price는 주문 당시 가격을 저장하는 의도적 중복입니다. 나중에 상품 가격이 바뀌어도 과거 주문 금액이 보존되어야 하기 때문입니다. 이처럼 비즈니스 요구사항에 의한 중복은 정상입니다.

3NF: 이행 종속 제거

기본 키 → 비키 컬럼 A → 비키 컬럼 B 처럼 간접 종속이 있으면 안 됩니다. B를 별도 테이블로 분리해야 합니다.

-- 나쁜 예: employees 테이블
-- employee_id → department_id → department_name
-- department_name이 employee_id가 아닌
-- department_id에 종속됨

-- 좋은 예: 부서를 별도 테이블로
CREATE TABLE departments (
  id   SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  budget NUMERIC(15, 2)
);

CREATE TABLE employees (
  id            SERIAL PRIMARY KEY,
  name          VARCHAR(100) NOT NULL,
  department_id INT REFERENCES departments(id)
  -- department_name은 여기 없음
);

부서 이름을 수정할 때 departments 테이블 한 행만 바꾸면 모든 직원에게 반영됩니다.

언제 역정규화가 맞을까?

정규화가 항상 정답은 아닙니다. 조회 성능이 중요할 때는 의도적으로 중복을 허용하는 **역정규화(Denormalization)**를 선택하기도 합니다.

-- 예 1: 댓글 수를 매번 COUNT()로 집계하기 힘들면
-- posts 테이블에 comment_count 컬럼을 두고 캐싱
ALTER TABLE posts ADD COLUMN comment_count INT DEFAULT 0;

-- 댓글 추가 시 트리거로 자동 업데이트
CREATE OR REPLACE FUNCTION update_comment_count()
RETURNS TRIGGER AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    UPDATE posts SET comment_count = comment_count + 1 WHERE id = NEW.post_id;
  ELSIF TG_OP = 'DELETE' THEN
    UPDATE posts SET comment_count = comment_count - 1 WHERE id = OLD.post_id;
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;

-- 예 2: 대용량 분석 쿼리용 요약 테이블
-- 원본 데이터를 정규화해서 저장하되,
-- 집계 결과는 별도 테이블에 미리 계산해두기

설계 원칙: 정규화로 시작하세요. 성능 문제가 실제로 발생하면, 그때 측정하고 필요한 부분만 선택적으로 역정규화하세요. 섣불리 역정규화하면 데이터 정합성 문제가 생깁니다.

실무에서 자주 보는 정규화 실수

실수 증상 해결
태그를 콤마로 구분해 TEXT에 저장 특정 태그 검색, 태그 수 집계 불가 별도 태그 테이블 + 연결 테이블
상태를 숫자 코드로만 저장 코드 의미를 앱 코드에서만 관리 ENUM 타입 또는 별도 상태 테이블
여러 용도의 "메타데이터"를 하나 컬럼에 JSON 특정 속성 쿼리 어려움 자주 조회하는 속성은 컬럼으로
주소를 하나의 TEXT로 도시/지역별 쿼리 불가 street, city, state, country로 분리

댓글

... 으로 작성됩니다

댓글을 불러오는 중...