본문 바로가기
TYLER SONGBlog
← Blog index
CS학습7 min read

2NF: 부분 함수 종속을 제거해 복합 키 테이블의 중복을 없애는 정규화

제2정규형(2NF)은 1NF를 만족하는 릴레이션에서 기본키가 아닌 속성이 후보키의 일부에만 종속되는 부분 함수 종속(Partial Dependency)을 제거한 상태다. 복합 키를 가진 테이블에서 중복 저장과 갱신 이상(Anomaly)이 발생하는 원인을 설명하고, PostgreSQL로 테이블을 분해하는 과정을 보여 준다. 분해 후에도 정보가 손실되지 않는지

By 송민성

1. 개념

제2정규형(Second Normal Form, 2NF)은 다음 두 조건을 모두 만족하는 릴레이션이다.

  1. 제1정규형(1NF)을 만족한다.
  2. 모든 비주요 속성(non-prime attribute)이 어떤 후보키(Candidate Key)에도 부분 함수 종속(Partial Functional Dependency)되지 않는다. 즉, 비주요 속성은 후보키 전체에 완전 함수 종속(Full Functional Dependency)이어야 한다.

용어를 정리하면 다음과 같다.

  • 함수 종속(Functional Dependency, FD): 속성 집합 X의 값이 정해지면 Y의 값이 하나로 정해질 때 X → Y로 쓴다.
  • 부분 함수 종속: 후보키가 복합키 (A, B)일 때, 비주요 속성 C가 A → C처럼 키의 일부만으로 결정되는 경우다.
  • 비주요 속성: 어떤 후보키에도 포함되지 않는 속성이다.

후보키가 단일 속성인 릴레이션은 키의 "일부"가 존재할 수 없으므로 1NF만 만족하면 자동으로 2NF를 만족한다. 2NF 위반은 복합 후보키가 있을 때만 발생한다.

2. 왜 사용하는가

부분 함수 종속이 있으면 같은 사실이 여러 행에 반복 저장되어 세 가지 이상 현상(Anomaly)이 생긴다. 주문 항목 테이블 order_items(order_id, product_id, product_name, quantity)를 예로 들자. 기본키는 (order_id, product_id)이고, product_name은 product_id만으로 결정된다.

  • 갱신 이상(Update Anomaly): 상품명을 바꾸면 해당 상품이 등장한 모든 행을 수정해야 한다. 일부만 수정하면 데이터가 서로 모순된다.
  • 삽입 이상(Insertion Anomaly): 아직 주문되지 않은 상품은 order_id가 없어 기본키를 채울 수 없으므로 저장할 수 없다.
  • 삭제 이상(Deletion Anomaly): 어떤 상품의 마지막 주문 행을 지우면 그 상품의 이름 정보도 함께 사라진다.

2NF로 분해하면 상품 정보는 한 곳에만 저장되어 이 문제들이 사라진다.

3. 동작 원리

2NF 판정과 분해는 다음 순서로 진행한다.

  1. 후보키를 찾는다. 위 예에서는 (order_id, product_id)다.
  2. 함수 종속을 나열한다.

- (order_id, product_id) → quantity (완전 종속) - product_id → product_name (부분 종속, 위반)

  1. 부분 종속을 만드는 결정자(product_id)와 그에 종속된 속성(product_name)을 별도 릴레이션으로 분리한다.
  2. 원래 릴레이션에는 결정자를 외래키(Foreign Key)로 남겨, 조인으로 원본을 복원할 수 있게 한다.

분해 결과는 다음과 같다.

  • products(product_id PK, product_name)
  • order_items(order_id, product_id FK, quantity, PK(order_id, product_id))

이 분해는 무손실 조인 분해(Lossless-Join Decomposition)다. 공통 속성 product_id가 products의 키이므로, 두 테이블을 조인하면 원본과 같은 행 집합이 복원된다.

4. 코드 예제

PostgreSQL 기준으로 위반 상태를 만들고, 이상 현상을 확인한 뒤 분해한다.

sql
-- 1) 2NF 위반 테이블 CREATE TABLE order_items_bad ( order_id INT, product_id INT, product_name TEXT NOT NULL, quantity INT NOT NULL, PRIMARY KEY (order_id, product_id) ); INSERT INTO order_items_bad VALUES (1, 10, 'Keyboard', 2), (2, 10, 'Keyboard', 1), (2, 20, 'Mouse', 3); -- 갱신 이상: 일부 행만 바뀌면 같은 상품의 이름이 달라진다 UPDATE order_items_bad SET product_name = 'Mech Keyboard' WHERE order_id = 1 AND product_id = 10; SELECT DISTINCT product_id, product_name FROM order_items_bad WHERE product_id = 10; -- 두 개의 이름이 나온다 -- 2) 2NF로 분해 CREATE TABLE products ( product_id INT PRIMARY KEY, product_name TEXT NOT NULL ); CREATE TABLE order_items ( order_id INT, product_id INT REFERENCES products(product_id), quantity INT NOT NULL, PRIMARY KEY (order_id, product_id) ); INSERT INTO products (product_id, product_name) SELECT DISTINCT ON (product_id) product_id, product_name FROM order_items_bad ORDER BY product_id, order_id DESC; INSERT INTO order_items (order_id, product_id, quantity) SELECT order_id, product_id, quantity FROM order_items_bad; -- 3) 상품명 변경은 한 행만 수정하면 된다 UPDATE products SET product_name = 'Mech Keyboard' WHERE product_id = 10; -- 4) 조인으로 원본 형태 복원 SELECT oi.order_id, oi.product_id, p.product_name, oi.quantity FROM order_items oi JOIN products p USING (product_id) ORDER BY oi.order_id, oi.product_id;

분해 전 DISTINCT ON으로 상품별 이름을 하나 고르는 부분에서, 이미 모순된 데이터가 있었다면 어떤 값을 정답으로 볼지 사람이 결정해야 한다.

5. 시간 복잡도 또는 성능 특성

2NF는 알고리즘이 아니라 설계 기준이므로 고정된 시간 복잡도는 없고, 성능에는 장단점이 함께 있다.

  • 저장 공간: 반복되던 product_name 같은 문자열이 한 번만 저장되어 줄어든다.
  • 쓰기: 중복 속성 갱신이 한 행의 수정으로 끝난다. 여러 행을 건드리지 않으므로 잠금 범위와 쓰기량이 줄어든다.
  • 읽기: 원본 형태가 필요하면 조인이 필요하다. products.product_id와 order_items.product_id에 인덱스가 있으면(기본키와 조회용 인덱스) 조인 비용은 일반적으로 크지 않지만, 조인 자체는 단일 테이블 조회보다 추가 작업이다.
  • 외래키 검사: order_items에 삽입할 때 products에 해당 키가 있는지 확인하므로 약간의 비용이 든다. 대신 정합성이 DB 차원에서 보장된다.

6. 실무 사용 사례

  • 주문/주문항목 모델: orders, order_items, products를 분리하는 것이 전형적인 2NF 설계다.
  • 수강 신청: (student_id, course_id)가 키인 테이블에 course_name을 두면 위반이므로 courses 테이블로 분리한다.
  • 다대다(N:M) 연결 테이블: 연결 테이블에는 관계 자체의 속성(수량, 가입일 등)만 두고, 각 엔티티 속성은 각 엔티티 테이블에 둔다.
  • 대리키(Surrogate Key) 사용: ORM에서 흔히 쓰는 단일 id 기본키는 부분 종속이 생길 여지를 줄인다. 다만 (order_id, product_id)에 대한 유니크 제약은 별도로 걸어야 중복 항목을 막는다.

7. 주의할 점

  • 단일 컬럼 기본키라도 다른 후보키가 복합키라면 판정 대상이다. 2NF는 기본키뿐 아니라 모든 후보키 기준으로 확인한다.
  • 2NF를 만족해도 이행 함수 종속(Transitive Dependency)은 남을 수 있다. 예를 들어 product_id → category_id → category_name은 3NF에서 다룬다.
  • 주문 시점의 가격이나 상품명을 보존해야 하는 경우는 의도적인 복제다. 현재 상품 가격은 products.price에, 주문 당시 가격은 order_items.unit_price에 두는 것은 서로 다른 사실이므로 위반이 아니다. 종속 관계를 의미 기준으로 판단해야 한다.
  • 함수 종속은 현재 데이터가 아니라 업무 규칙(스키마의 의미)에서 도출한다. 샘플 데이터에서 우연히 성립하는 패턴으로 판단하면 안 된다.
  • 읽기 중심의 분석 시스템에서는 성능을 위해 의도적으로 비정규화(Denormalization)하기도 한다. 이때는 중복 갱신을 보장하는 파이프라인이 필요하다.

8. 핵심 정리

  • 2NF = 1NF + 비주요 속성의 부분 함수 종속 제거.
  • 복합 후보키가 있을 때만 위반이 생긴다.
  • 위반 시 갱신·삽입·삭제 이상이 발생한다.
  • 해결책은 키의 일부에 종속된 속성을 결정자와 함께 별도 테이블로 분리하고, 외래키로 연결하는 무손실 분해다.
  • 이행 종속은 2NF로 해결되지 않으며 3NF에서 다룬다.
  • 주문 당시 값처럼 의미가 다른 데이터의 복제는 위반이 아니므로 업무 규칙으로 판단한다.