정규화: 1NF, 2NF, 3NF, BCNF와 이상 현상

src/content/documents/data-analysis/database-normalization-1nf-2nf-3nf.json

관계형 데이터베이스를 설계할 때 중요한 개념 중 하나가 **정규화(Normalization)**입니다.

정규화는 단순히 테이블을 여러 개로 쪼개는 작업이 아닙니다.

핵심 목적은 다음과 같습니다.

중복되는 데이터를 줄이고, 데이터를 추가·수정·삭제할 때 생기는 이상 현상을 줄이기 위해 테이블 구조를 정리하는 것

이번 글에서는 MMORPG 게임의 플레이어, 길드, 아이템, 인벤토리 데이터를 이용해 정규화를 단계별로 살펴보겠습니다.


정규화하지 않은 데이터부터 보기

먼저 게임 데이터를 아래처럼 하나의 테이블에 전부 저장한다고 생각해봅시다.

PlayerData

player_id

nickname

guild_id

guild_name

guild_master

items

1

천기사

10

용기사단

드래곤

철검, 포션, 방패

2

흑마법사

20

마법협회

멀린

지팡이, 포션

3

궁수왕

10

용기사단

드래곤

활, 포션

사람이 보기에는 꽤 자연스럽습니다.

하지만 데이터베이스 관점에서는 문제가 많습니다.

우선 items 컬럼을 보면 한 칸에 여러 값이 들어가 있습니다.

철검, 포션, 방패

또한 용기사단, 드래곤이라는 정보가 반복됩니다.

플레이어가 수천 명이 된다면 같은 길드 정보가 수천 번 반복될 수 있습니다.


제1정규형 1NF

핵심: 한 칸에는 하나의 값만

제1정규형의 핵심은 매우 단순합니다.

각 컬럼에는 하나의 값만 들어가야 한다.

다음 테이블은 1NF를 만족하지 않습니다.

player_id

nickname

items

1

천기사

철검, 포션, 방패

2

흑마법사

지팡이, 포션

items 하나의 컬럼 안에 여러 개의 값이 들어가기 때문입니다.


왜 문제가 될까?

예를 들어 포션을 가진 플레이어만 검색하고 싶다고 생각해봅시다.

데이터가 이렇게 들어가 있으면

철검, 포션, 방패

문자열 내부를 따로 검색해야 합니다.

아이템 하나를 삭제하거나 수량을 관리하기도 어렵습니다.

그래서 값을 각각 분리합니다.

1NF 적용 후

player_id

nickname

item

1

천기사

철검

1

천기사

포션

1

천기사

방패

2

흑마법사

지팡이

2

흑마법사

포션

이제 하나의 셀에는 하나의 값만 들어갑니다.

❌ 철검, 포션, 방패



⭕ 철검
⭕ 포션
⭕ 방패

따라서 제1정규형을 만족합니다.


제2정규형 2NF

제2정규형부터는 함수적 종속을 이해해야 합니다.

함수적 종속은 어렵게 들리지만 의미는 단순합니다.

A를 알면 B가 하나로 결정되는 관계

예를 들어 다음 관계가 있습니다.

player_id → nickname

player_id = 1이면 닉네임이 항상 천기사로 결정된다면 함수적 종속 관계입니다.


다음 인벤토리 테이블을 보자

player_id

item_id

nickname

item_name

quantity

1

101

천기사

철검

1

1

102

천기사

포션

5

2

102

흑마법사

포션

10

2

103

흑마법사

지팡이

1

이 테이블에서는

(player_id, item_id)

두 컬럼을 함께 사용해야 하나의 행을 구별할 수 있다고 하겠습니다.

즉 기본키가 다음과 같습니다.

PK = (player_id, item_id)

각 컬럼이 무엇에 의존하는지 확인해보기

데이터를 보면 다음 관계가 성립합니다.

player_id → nickname

예를 들어

player_id

nickname

1

천기사

1

천기사

player_id = 1만 알아도 닉네임은 항상 천기사입니다.


아이템도 마찬가지입니다.

item_id → item_name

item_id

item_name

102

포션

102

포션

item_id = 102만 알아도 포션이라는 것을 알 수 있습니다.

하지만 수량은 다릅니다.

(player_id, item_id) → quantity

예를 들어 item_id = 102만 알아서는 수량을 알 수 없습니다.

player_id

item_id

quantity

1

102

5

2

102

10

천기사는 포션 5개를 가지고 있고, 흑마법사는 10개를 가지고 있습니다.

따라서 수량은

player_id + item_id

둘 다 알아야 결정됩니다.


부분 함수 종속

현재 관계를 정리하면 다음과 같습니다.

(player_id, item_id) → quantity

player_id → nickname

item_id → item_name

nickname은 복합키 전체가 아니라 player_id 하나에만 의존합니다.

item_name 역시 item_id 하나에만 의존합니다.

이것을 부분 함수 종속이라고 합니다.

복합 PK (player_id, item_id)nicknameitem_namequantityplayer_id만으로 결정item_id만으로 결정복합키 전체로 결정
복합키의 부분 함수 종속

2NF로 분리하기

부분 함수 종속을 제거하기 위해 테이블을 나눕니다.

Player

player_id

nickname

1

천기사

2

흑마법사

Item

item_id

item_name

101

철검

102

포션

103

지팡이

Inventory

player_id

item_id

quantity

1

101

1

1

102

5

2

102

10

2

103

1

Inventory 안에서는 이제

(player_id, item_id) → quantity

만 남습니다.

이것이 제2정규형입니다.

2NF = 1NF를 만족하면서 복합키 일부에만 의존하는 속성을 제거한다.


2NF 전후 비교

정규화 전

player_id

item_id

nickname

item_name

quantity

1

101

천기사

철검

1

1

102

천기사

포션

5

2

102

흑마법사

포션

10

문제는 같은 데이터가 반복된다는 것입니다.

천기사가 아이템을 100개 가지고 있다면 천기사라는 닉네임도 100번 반복됩니다.


정규화 후

Player

player_id

nickname

1

천기사

2

흑마법사

Inventory

player_id

item_id

quantity

1

101

1

1

102

5

2

102

10

닉네임은 Player에서 한 번만 저장됩니다.


제3정규형 3NF

이번에는 다음 Player 테이블이 있다고 해봅시다.

player_id

nickname

guild_id

guild_name

guild_master

1

천기사

10

용기사단

드래곤

2

흑마법사

20

마법협회

멀린

3

궁수왕

10

용기사단

드래곤

4

성기사

10

용기사단

드래곤

기본키는 player_id입니다.

그런데 컬럼 간 관계를 살펴보면 다음과 같습니다.

player_id → nickname
player_id → guild_id

여기까지는 자연스럽습니다.

하지만

guild_id → guild_name
guild_id → guild_master

도 성립합니다.

즉 다음 구조가 만들어집니다.

player_idguild_idguild_nameguild_master
이행적 함수 종속

이행적 함수 종속

guild_name은 사실 player_id가 직접 결정하는 정보가 아닙니다.

중간에 guild_id가 있습니다.

player_id → guild_id → guild_name

이러한 관계를 이행적 함수 종속이라고 합니다.

문제가 되는 이유는 데이터 중복입니다.

player_id

guild_id

guild_name

1

10

용기사단

3

10

용기사단

4

10

용기사단

같은 길드 이름이 계속 반복됩니다.


3NF로 분리하기

길드 정보를 별도의 테이블로 분리합니다.

Player

player_id

nickname

guild_id

1

천기사

10

2

흑마법사

20

3

궁수왕

10

4

성기사

10

Guild

guild_id

guild_name

guild_master

10

용기사단

드래곤

20

마법협회

멀린

이제 Player는 플레이어 정보만 가지고 있고, Guild는 길드 정보를 담당합니다.

3NF = 2NF를 만족하면서 일반 속성이 다른 일반 속성을 결정하는 이행적 함수 종속을 제거한다.


이상 현상이란?

정규화가 필요한 가장 큰 이유 중 하나가 바로 **이상 현상(Anomaly)**입니다.

대표적으로 다음 세 가지가 있습니다.

삽입 이상
수정 이상
삭제 이상

게임의 길드 데이터를 이용해 각각 살펴보겠습니다.


삽입 이상

다음과 같은 테이블이 있다고 해봅시다.

PlayerGuild

player_id

nickname

guild_id

guild_name

1

천기사

10

용기사단

2

흑마법사

20

마법협회

이번에 새로운 길드가 만들어졌습니다.

guild_id = 30
guild_name = 암흑기사단

그런데 아직 가입한 플레이어가 없습니다.

현재 구조에서는 길드 정보를 저장하려면 Player 데이터도 필요합니다.

억지로 넣으면 다음처럼 될 수 있습니다.

player_id

nickname

guild_id

guild_name

NULL

NULL

30

암흑기사단

하지만 player_id가 PK라면 NULL을 넣을 수도 없습니다.

길드 정보만 저장하고 싶은데 플레이어가 없어서 저장할 수 없는 문제

가 발생합니다.

이것이 삽입 이상입니다.


정규화 후 삽입 이상 해결

Guild 테이블을 따로 만들면 문제가 사라집니다.

Guild

guild_id

guild_name

10

용기사단

20

마법협회

30

암흑기사단

플레이어가 없어도 길드 자체를 저장할 수 있습니다.


수정 이상

이번에는 용기사단 이름을 변경한다고 해봅시다.

현재 데이터가 다음과 같습니다.

player_id

nickname

guild_id

guild_name

1

천기사

10

용기사단

3

궁수왕

10

용기사단

4

성기사

10

용기사단

게임 운영자가 길드명을 다음과 같이 변경했습니다.

용기사단 → 황금용기사단

그러면 관련된 모든 행을 수정해야 합니다.

정상적으로 수정하면

player_id

nickname

guild_id

guild_name

1

천기사

10

황금용기사단

3

궁수왕

10

황금용기사단

4

성기사

10

황금용기사단

이 됩니다.

하지만 실수로 한 행을 빼먹으면 어떻게 될까요?

player_id

nickname

guild_id

guild_name

1

천기사

10

황금용기사단

3

궁수왕

10

용기사단

4

성기사

10

황금용기사단

같은 guild_id = 10인데 길드 이름이 두 개가 되어버렸습니다.

guild_id 10

→ 황금용기사단
→ 용기사단

어느 것이 진짜 데이터인지 알기 어려워집니다.

이것이 수정 이상입니다.


정규화 후 수정 이상 해결

길드 정보를 별도 테이블에 두면 됩니다.

Guild

guild_id

guild_name

10

용기사단

20

마법협회

수정할 때는 한 행만 변경합니다.

guild_id

guild_name

10

황금용기사단

20

마법협회

Player에서는 guild_id = 10만 참조하므로 모든 플레이어에게 변경된 길드 이름이 동일하게 적용됩니다.


삭제 이상

다음 데이터를 살펴봅시다.

player_id

nickname

guild_id

guild_name

1

천기사

10

용기사단

2

흑마법사

20

마법협회

이번에 흑마법사가 게임을 탈퇴했습니다.

그러면 다음 행을 삭제합니다.

2 | 흑마법사 | 20 | 마법협회

삭제 후에는

player_id

nickname

guild_id

guild_name

1

천기사

10

용기사단

만 남습니다.

문제는 마법협회 자체는 아직 존재할 수도 있다는 것입니다.

하지만 마지막 길드원을 삭제하면서

guild_id = 20
guild_name = 마법협회

정보까지 같이 사라졌습니다.

이것이 삭제 이상입니다.


이상 현상 한 번에 비교

이상 현상

상황

발생하는 문제

삽입 이상

새로운 길드만 만들고 싶음

플레이어가 없으면 길드 저장이 어려움

수정 이상

길드 이름 변경

여러 행을 수정해야 하며 일부만 수정될 수 있음

삭제 이상

마지막 길드원 삭제

길드 정보까지 사라질 수 있음

정규화를 하면 Player와 Guild를 분리하여 이런 문제를 크게 줄일 수 있습니다.


정규화 전과 후 전체 비교

정규화 전

player_id

nickname

guild_id

guild_name

item_id

item_name

quantity

1

천기사

10

용기사단

101

철검

1

1

천기사

10

용기사단

102

포션

5

3

궁수왕

10

용기사단

102

포션

3

2

흑마법사

20

마법협회

103

지팡이

1

중복을 확인해보면

천기사 → 반복
용기사단 → 반복
포션 → 반복

이 계속 발생합니다.


정규화 후 구조

Player

player_id

nickname

guild_id

1

천기사

10

2

흑마법사

20

3

궁수왕

10

Guild

guild_id

guild_name

10

용기사단

20

마법협회

Item

item_id

item_name

101

철검

102

포션

103

지팡이

Inventory

player_id

item_id

quantity

1

101

1

1

102

5

3

102

3

2

103

1

이제 역할이 명확하게 나뉩니다.

Player
→ 플레이어 자체 정보

Guild
→ 길드 자체 정보

Item
→ 아이템 자체 정보

Inventory
→ 플레이어가 어떤 아이템을 몇 개 가지고 있는지

BCNF

BCNF는 3NF보다 조금 더 엄격한 정규형입니다.

핵심은 다음 문장입니다.

모든 결정자가 후보키여야 한다.

여기서 결정자는

A → B

관계에서 A를 의미합니다.

A를 알면 B가 결정되므로 A가 결정자입니다.


후보키란?

후보키는 테이블의 각 행을 유일하게 식별할 수 있는 최소한의 컬럼 집합입니다.

예를 들어 Player가 다음과 같다고 해봅시다.

player_id

account_email

nickname

1

knight@test.com

천기사

2

mage@test.com

흑마법사

player_id가 유일하고 account_email도 유일하다면

player_id

account_email

둘 다 후보키가 될 수 있습니다.

이 중 하나를 선택하여 Primary Key로 사용할 수 있습니다.


BCNF 예시

다음과 같은 레이드 테이블이 있다고 가정하겠습니다.

raid_id

role

player

R1

Tank

천기사

R1

Heal

성직자

R2

Tank

흑기사

R2

Heal

사제왕

그리고 게임 규칙상

player → role

이 성립한다고 가정해봅시다.

즉 한 플레이어는 항상 동일한 역할만 맡습니다.

예를 들어 천기사는 언제나 Tank입니다.

그렇다면 Player가 Role을 결정합니다.

천기사 → Tank
성직자 → Heal

이때 Player가 결정자입니다.

BCNF에서는 이러한 결정자가 후보키인지까지 검사합니다.

후보키가 아닌 결정자가 존재하면 테이블을 추가로 분리합니다.

예를 들어

PlayerRole

player

role

천기사

Tank

성직자

Heal

흑기사

Tank

RaidPlayer

raid_id

player

R1

천기사

R1

성직자

R2

흑기사

처럼 관계를 분리할 수 있습니다.

처음 공부할 때는 BCNF를 다음처럼 기억하면 충분합니다.

3NF
→ 일반 컬럼 사이의 이행적 종속을 제거

BCNF
→ 결정자까지 검사
→ 결정자는 후보키여야 함

제4정규형 4NF

제4정규형에서는 다치 종속이라는 개념을 다룹니다.

게임 플레이어가 다음과 같은 정보를 가진다고 해봅시다.

천기사

사용 가능한 무기
→ 검
→ 활

보유 펫
→ 슬라임
→ 드래곤

이 둘은 서로 직접적인 관계가 없습니다.

그런데 하나의 테이블에 저장하면 다음과 같이 됩니다.

player

weapon

pet

천기사

슬라임

천기사

드래곤

천기사

슬라임

천기사

드래곤

무기 2개 × 펫 2개 때문에 총 4개의 조합이 만들어집니다.


4NF 적용

둘은 서로 독립적인 정보이므로 분리합니다.

PlayerWeapon

player

weapon

천기사

천기사

PlayerPet

player

pet

천기사

슬라임

천기사

드래곤

이제 불필요한 조합이 사라집니다.

4NF = 서로 독립적인 다치 종속을 분리한다.


4NF에서 데이터가 얼마나 줄어드는지

예를 들어 한 플레이어가

무기 10개
펫 10마리

를 가지고 있다고 해봅시다.

한 테이블에 저장하면

10 × 10 = 100행

이 필요할 수 있습니다.

하지만 테이블을 분리하면

PlayerWeapon = 10행
PlayerPet = 10행

총 20행

이면 됩니다.

즉 독립된 N개의 값과 M개의 값을 한 테이블에 섞으면 N × M 형태로 데이터가 증가할 수 있습니다.


제5정규형 5NF

제5정규형은 조인 종속을 다룹니다.

쉽게 표현하면

테이블을 더 작은 관계들로 분리했을 때 다시 JOIN하면 원래 의미를 정확히 복원할 수 있는가?

를 다루는 단계입니다.

예를 들어 게임 거래 시스템에서

상인
아이템
지역

세 가지 관계가 복잡하게 연결되어 있다고 가정할 수 있습니다.

특정 조건에서

MerchantItem
MerchantRegion
ItemRegion

처럼 여러 테이블로 분리한 후 JOIN하여 원래 데이터를 정확히 복원할 수 있는지를 검사합니다.

5NF는 비교적 특수한 경우를 다루기 때문에 데이터베이스 기초에서는 보통 1NF~3NF와 BCNF를 우선적으로 학습합니다.


정규형별 데이터 변화 다시 보기

처음 데이터가 다음과 같다고 생각해봅시다.

비정규형

player

items

guild

천기사

철검, 포션, 방패

용기사단

1NF

하나의 셀에 하나의 값만 넣습니다.

player

item

guild

천기사

철검

용기사단

천기사

포션

용기사단

천기사

방패

용기사단

2NF

Player와 Item 정보를 분리합니다.

Player
Item
Inventory

3NF

길드 정보도 분리합니다.

Player
Guild
Item
Inventory

BCNF

후보키가 아닌 결정자가 존재하는지 추가로 검사합니다.

4NF

서로 독립적인 여러 값들의 관계를 분리합니다.

5NF

복잡한 조인 관계를 더 세밀하게 분해합니다.


정규형별 핵심 한눈에 보기

단계

핵심 질문

해결하는 문제

비정규형

데이터가 아무렇게나 섞여 있는가?

중복, 복수 값

1NF

한 칸에 값이 하나인가?

반복 그룹

2NF

복합키 일부에만 의존하는 컬럼이 있는가?

부분 함수 종속

3NF

일반 컬럼이 다른 일반 컬럼을 결정하는가?

이행적 함수 종속

BCNF

모든 결정자가 후보키인가?

3NF보다 엄격한 함수 종속

4NF

독립적인 다중 값이 한 테이블에 섞여 있는가?

다치 종속

5NF

더 분해해도 JOIN으로 의미를 복원할 수 있는가?

조인 종속


시험 문제에서는 이렇게 판단하면 쉽다

다음 테이블을 받았다고 생각해봅시다.

학번

과목번호

학생이름

과목명

교수

1

C01

철수

DB

김교수

1

C02

철수

Java

이교수

2

C01

영희

DB

김교수

PK가

(학번, 과목번호)

라고 한다면 다음 관계를 찾습니다.

학번 → 학생이름
과목번호 → 과목명
과목번호 → 교수

복합키의 일부만으로 결정되는 값들이 존재합니다.

따라서 2NF 위반입니다.

다음처럼 분리할 수 있습니다.

Student

학번

학생이름

1

철수

2

영희

Course

과목번호

과목명

교수

C01

DB

김교수

C02

Java

이교수

Enrollment

학번

과목번호

1

C01

1

C02

2

C01

이런 식으로 PK가 무엇인지 찾고 각 컬럼이 무엇에 의존하는지를 따지는 것이 정규화 문제의 핵심입니다.


정규화를 판단하는 순서

정규화 문제를 보면 다음 순서로 확인하면 이해하기 쉽습니다.

① 기본키가 무엇인가?



② 한 칸에 여러 값이 들어있는가?
→ 그렇다면 1NF 문제



③ 기본키가 복합키인가?



④ 복합키 일부만으로 결정되는 컬럼이 있는가?
→ 그렇다면 2NF 문제



⑤ 일반 컬럼이 다른 일반 컬럼을 결정하는가?
→ 그렇다면 3NF 문제



⑥ 후보키가 아닌 결정자가 존재하는가?
→ BCNF 확인

정규화의 핵심은 “중복 자체”가 아니다

정규화를 처음 배우면

데이터가 중복되면 무조건 잘못된 것인가?

라고 생각하기 쉽습니다.

하지만 단순한 값의 반복만 보고 정규화를 판단하는 것은 아닙니다.

예를 들어 Inventory에서

player_id

item_id

1

102

2

102

3

102

item_id = 102가 여러 번 반복됩니다.

하지만 이것은 여러 플레이어가 동일한 포션을 가지고 있다는 관계 자체를 표현하기 위한 정상적인 중복입니다.

문제가 되는 것은 다음과 같은 형태입니다.

player_id

item_id

item_name

price

1

102

포션

100

2

102

포션

100

3

102

포션

100

item_id = 102를 알면 이미

item_name = 포션
price = 100

이 결정됩니다.

따라서 아이템의 고유 정보가 Inventory에 계속 반복되는 것이 문제입니다.

이 정보는 Item 테이블에 한 번만 저장하는 것이 더 적절합니다.


최종 구조

게임 데이터베이스를 적절히 정규화하면 다음과 같은 구조를 만들 수 있습니다.

Guild
──────────────
guild_id PK
guild_name
guild_master

       │ 1

       N
Player
──────────────
player_id PK
nickname
level
guild_id FK

       │ 1

       N
Inventory
──────────────
player_id PK, FK
item_id   PK, FK
quantity

       │ N

       1
Item
──────────────
item_id PK
item_name
price
GUILDPK guild_idguild_nameguild_masterPLAYERPK player_idnickname · levelFK guild_idINVENTORYPK/FK player_idPK/FK item_idquantityITEMPK item_iditem_name · price1N1NN1
정규화된 게임 데이터베이스 ERD

정리

정규화는 단순히 테이블을 여러 개 만드는 기술이 아닙니다.

데이터 사이의 의존 관계를 분석해서 각 정보가 가장 적절한 위치에 저장되도록 구조를 만드는 과정입니다.

가장 중요한 단계만 다시 정리하면 다음과 같습니다.

정규형

가장 쉽게 기억하는 방법

1NF

한 칸에 하나

2NF

복합키 일부에만 의존하면 분리

3NF

일반 컬럼끼리 의존하면 분리

BCNF

모든 결정자는 후보키

4NF

독립적인 여러 값들을 분리

5NF

복잡한 JOIN 관계를 분리

그리고 정규화를 하는 가장 현실적인 이유는 다음 세 가지 이상 현상을 방지하기 위해서입니다.

이상 현상

의미

삽입 이상

필요한 데이터만 추가하기 어려움

수정 이상

같은 정보를 여러 곳에서 수정해야 함

삭제 이상

데이터를 하나 삭제했는데 다른 중요한 정보까지 사라짐

결국 좋은 관계형 데이터 모델은 다음과 같은 상태를 목표로 합니다.

Player 정보는 Player에

Guild 정보는 Guild에

Item 정보는 Item에

Player와 Item의 관계는 Inventory에

즉,

각 데이터는 자신이 있어야 할 테이블에 한 번만 저장하고, 필요한 관계는 PK와 FK로 연결한다.

이 관점으로 정규화를 이해하면 1NF, 2NF, 3NF가 단순한 암기 항목이 아니라 실제 데이터베이스 구조를 개선하는 과정으로 보이기 시작합니다.

댓글 0

댓글을 불러오는 중…