데이터베이스 제약조건 PK, FK, UK

src/content/documents/data-analysis/relational-model-keys-and-relationships.json

게임 데이터베이스를 만든다고 생각해보자.
게임에는 수많은 데이터가 존재한다.

  • 플레이어

  • 캐릭터

  • 아이템

  • 길드

  • 퀘스트

  • 인벤토리

예를 들어 플레이어 정보를 다음과 같이 저장할 수 있다.

player_id

nickname

email

level

1001

천검

player1@game.com

35

1002

마법사킴

player2@game.com

21

1003

탱커박

player3@game.com

42

그런데 데이터베이스에 아무런 규칙도 없다면 문제가 발생할 수 있다.

player_id = 1001인 플레이어가 두 명이면?

같은 이메일로 계정이 여러 개 만들어지면?

존재하지 않는 플레이어에게 아이템을 지급하면?

이러한 잘못된 데이터가 저장되는 것을 막기 위해 데이터베이스에 **제약조건(Constraint)**을 설정한다.
그중 관계형 데이터베이스에서 가장 기본적으로 사용되는 것이 PK, FK, UK다.


제약조건(Constraint)이란?

제약조건은 말 그대로 데이터베이스에 저장되는 데이터가 반드시 지켜야 하는 규칙이다.
게임 운영자가 다음과 같은 규칙을 만들었다고 생각하면 쉽다.

"모든 플레이어는 서로 다른 고유번호를 가져야 한다."

"동일한 이메일로 두 개의 계정을 만들 수 없다."

"존재하지 않는 플레이어에게 아이템을 지급할 수 없다."

이러한 규칙을 애플리케이션 코드에서 검사할 수도 있지만, 데이터베이스 자체에도 규칙을 설정할 수 있다.

PRIMARY KEY
FOREIGN KEY
UNIQUE
NOT NULL
CHECK

이번에는 그중 데이터를 식별하고 연결하는 데 중요한 PK, FK, UK를 게임 데이터베이스를 통해 알아보자.


PK(Primary Key) — 플레이어의 고유번호

**Primary Key(PK, 기본키)**는 테이블에서 각 행(Row)을 고유하게 식별하기 위한 값이다.
게임의 플레이어 테이블을 생각해보자.

player_id

nickname

level

1001

천검

35

1002

마법사킴

21

1003

탱커박

42

여기서 player_id를 PK로 사용할 수 있다.

CREATE TABLE player (
    player_id INTEGER PRIMARY KEY,
    nickname VARCHAR(30),
    level INTEGER
);
player_id = 1001

게임 서버에서 이라고 하면 데이터베이스는 정확하게 한 명의 플레이어를 찾을 수 있다.


왜 닉네임을 PK로 사용하지 않을까?

닉네임도 중복을 금지하면 고유한 값으로 사용할 수 있다.
하지만 닉네임은 변경될 가능성이 있다.

천검

↓ 닉네임 변경

천상의검

반면 내부적으로 사용하는 player_id는 일반적으로 변경할 필요가 없다.

player_id = 1001

닉네임: 천검

닉네임: 천상의검

player_id = 1001  ← 그대로

그래서 실제 데이터베이스에서는 의미가 있는 이름보다 변하지 않는 ID 값을 PK로 사용하는 경우가 많다.


PK의 중요한 특징

PK에는 중요한 규칙이 있다.

① 중복될 수 없다

1001 | 천검
1001 | 마법사킴   ← 불가능

두 플레이어가 동일한 player_id를 가질 수 없다.

② NULL이 될 수 없다

NULL | 천검   ← 불가능

PK는 데이터를 식별해야 하기 때문에 값이 반드시 존재해야 한다.
즉 PK의 핵심은 다음과 같다.

PK = 이 데이터가 정확히 누구인지 식별하기 위한 고유한 값


서로 다른 Player ID 1001, 1002, 1003으로 기본키의 고유성을 보여 주는 RPG 캐릭터 도식

UK(Unique Key) — 이메일과 닉네임 중복 금지

이번에는 플레이어의 이메일을 생각해보자.

player_id

nickname

email

1001

천검

player1@game.com

1002

마법사킴

player2@game.com

1003

탱커박

player3@game.com

우리 게임에서는 하나의 이메일로 하나의 계정만 만들 수 있다고 가정하자.
그런데 PK는 이미 player_id다.
그렇다고 이메일의 중복을 허용하면 다음과 같은 데이터가 만들어질 수 있다.

1001 | 천검     | player1@game.com
1002 | 마법사킴 | player1@game.com

이때 사용할 수 있는 것이 UNIQUE 제약조건이다.

CREATE TABLE player (
    player_id INTEGER PRIMARY KEY,
    nickname VARCHAR(30),
    email VARCHAR(100) UNIQUE
);

이제 동일한 이메일을 다시 저장하려고 하면 데이터베이스가 거부한다.

player1@game.com  ← 이미 존재

player1@game.com  ← 저장 불가

UK(Unique Key)는 특정 컬럼의 값이 중복되지 않도록 만드는 역할을 한다.


PK와 UK는 뭐가 다른가?

둘 다 중복을 허용하지 않기 때문에 처음에는 비슷해 보인다.
게임 계정으로 비교하면 이해하기 쉽다.

Player

player_id = 1001
nickname  = 천검
email     = player1@game.com

여기서

player_id → PK
email     → UNIQUE

로 설정할 수 있다.
player_id플레이어라는 데이터를 대표해서 식별하는 값이다.
반면 email은 플레이어 자체를 대표하기 위해 만든 키라기보다 업무 규칙상 중복되면 안 되는 값이다.

구분

PK

UNIQUE

주요 목적

행을 대표하여 식별

값의 중복 방지

중복

불가능

불가능

NULL

불가능

DBMS에 따라 처리 차이 존재

한 테이블에 여러 개

불가능

가능

예시

player_id

email, nickname

예를 들어 다음처럼 여러 UNIQUE 제약조건을 둘 수도 있다.

CREATE TABLE player (
    player_id INTEGER PRIMARY KEY,
    nickname VARCHAR(30) UNIQUE,
    email VARCHAR(100) UNIQUE
);

그러면

player_id → 플레이어 식별
nickname  → 중복 금지
email     → 중복 금지

라는 서로 다른 역할을 수행한다.

참고로 UNIQUENULL 처리 방식은 DBMS마다 차이가 있다. PostgreSQL의 일반적인 UNIQUE 제약조건에서는 여러 NULL을 허용할 수 있으므로, 값 자체가 반드시 존재해야 한다면 NOT NULL도 함께 고려해야 한다.


FK(Foreign Key) — 다른 테이블과 연결하기

이번에는 플레이어의 인벤토리를 만들어보자.
게임에서는 플레이어 한 명이 여러 아이템을 가지고 있을 수 있다.

Player

player_id

nickname

1001

천검

1002

마법사킴

Inventory

inventory_id

player_id

item_name

1

1001

전설의 검

2

1001

체력 포션

3

1002

마법 지팡이

Inventory의 player_id를 보면 Player 테이블에도 같은 값이 존재한다.

Player

1001 | 천검


   │ 참조

Inventory

1 | 1001 | 전설의 검
2 | 1001 | 체력 포션

이때 Inventory의 player_id가 **FK(Foreign Key, 외래키)**가 된다.
SQL로는 다음처럼 설정할 수 있다.

CREATE TABLE inventory (
    inventory_id INTEGER PRIMARY KEY,
    player_id INTEGER NOT NULL,
    item_name VARCHAR(100),

    FOREIGN KEY (player_id)
        REFERENCES player(player_id)
);

핵심은 이 부분이다.

FOREIGN KEY (player_id)
REFERENCES player(player_id)

쉽게 읽으면 다음과 같다.

"Inventory의 player_id는 Player 테이블의 player_id를 참조한다."


FK가 없다면 어떤 문제가 생길까?

현재 존재하는 플레이어가 다음 두 명뿐이라고 해보자.

1001 | 천검
1002 | 마법사킴

그런데 실수로 다음 데이터를 저장한다.

Inventory

10 | 9999 | 전설의 검

문제가 있다.

player_id = 9999

→ 그런 플레이어가 없음

그러면 주인 없는 아이템 데이터가 만들어진다.
FK를 설정했다면 데이터베이스는 이를 막을 수 있다.

Inventory에 player_id = 9999 저장 요청



Player에 9999가 있는지 확인



없음 ❌



저장 거부

이처럼 서로 연결된 테이블의 데이터가 모순되지 않도록 유지하는 것을 **참조 무결성(Referential Integrity)**이라고 한다.


Player 1001의 인벤토리 연결은 허용하고 존재하지 않는 Player 9999의 연결은 거부하는 외래키 도식

PK와 FK는 같은 컬럼에 나타날 수도 있다

PK와 FK를 공부할 때 많이 헷갈리는 부분이 있다.

Player.player_id       → PK
Inventory.player_id    → FK

이 둘은 이름과 값이 같을 수 있지만 역할이 다르다.

┌─────────────────┐
│ Player          │
├─────────────────┤
│ PK player_id    │
│ nickname        │
└────────┬────────┘

         │ 1

         │ N

┌─────────────────┐
│ Inventory       │
├─────────────────┤
│ PK inventory_id │
│ FK player_id    │
│ item_name       │
└─────────────────┘

Player에서는 player_id

"이 플레이어가 누구인가?"

를 나타내므로 PK다.
Inventory에서는 player_id

"이 아이템의 주인이 누구인가?"

를 나타내며 Player를 참조하므로 FK다.


게임 데이터베이스를 조금 더 확장해보자

실제 게임처럼 구조를 조금 확장해보자.

Player

PK player_id
UK email
UK nickname
level

Item

PK item_id
item_name
price

Inventory

PK inventory_id
FK player_id
FK item_id
quantity

관계를 그림으로 표현하면 다음과 같다.

┌───────────────┐
│    Player     │
├───────────────┤
│ PK player_id  │
│ UK email      │
│ UK nickname   │
│ level         │
└───────┬───────┘

        │ 1

        │ N
┌───────▼───────┐
│   Inventory   │
├───────────────┤
│ PK inventory_id│
│ FK player_id  │
│ FK item_id    │
│ quantity      │
└───────┬───────┘
        │ N

        │ 1
┌───────▼───────┐
│     Item      │
├───────────────┤
│ PK item_id    │
│ item_name     │
│ price         │
└───────────────┘

여기에는 세 가지 키가 모두 사용되고 있다.
PK

player_id
item_id
inventory_id

각 테이블의 데이터를 고유하게 식별한다.
UK

email
nickname

게임 정책상 중복되면 안 되는 값을 제한한다.
FK

Inventory.player_id
Inventory.item_id

Inventory를 Player와 Item에 연결한다.


PLAYER, INVENTORY, ITEM 테이블을 연결해 PK는 고유 식별, UK는 중복 방지, FK는 테이블 연결 역할임을 보여 주는 게임 데이터베이스 도식

PK, FK, UK를 게임으로 기억하기

세 가지를 어렵게 외울 필요는 없다.
게임 캐릭터 하나를 생각하면 된다.

PK = 캐릭터 고유번호

"너 누구야?"

→ Player ID 1001

데이터를 식별한다.

UK = 중복 불가능한 닉네임

"이미 사용 중인 닉네임입니다."

특정 값의 중복을 막는다.

FK = 아이템의 주인번호

"이 검 누구 거야?"

→ Player ID 1001의 아이템

다른 테이블의 데이터를 참조하고 연결한다.


마무리

PK, FK, UK는 모두 데이터에 규칙을 부여하기 위한 중요한 제약조건이다.
하지만 각각의 목적은 명확하게 다르다.

Key

역할

게임 예시

PK

데이터를 고유하게 식별

플레이어 고유 ID

UK

특정 값의 중복 방지

이메일, 닉네임

FK

다른 테이블을 참조하고 연결

아이템 소유자의 player_id

한 문장씩 기억하면 간단하다.

PK는 "누구인지" 구별한다.

UK는 "같은 값"이 들어오는 것을 막는다.

FK는 "누구와 연결되어 있는지" 나타낸다.

이 세 가지를 이용하면 단순히 데이터를 저장하는 것을 넘어 중복된 계정이나 존재하지 않는 플레이어의 아이템처럼 잘못된 데이터가 만들어지는 것을 데이터베이스 단계에서 방지할 수 있다.
그리고 이러한 PK와 FK를 기반으로 테이블 간의 1:1, 1:N, N:M 관계가 만들어지면서 본격적인 관계형 데이터 모델링으로 이어진다.

댓글 0

댓글을 불러오는 중…