자연 키와 대리 키
안정적인 내부 식별자와 이메일·외부 계정의 업무상 유일성을 분리해 변화에 강한 회원 모델을 만듭니다.
이전 문서의 이메일 기본 키 모델은 참조 무결성 덕분에 잘못된 변경을 막았지만, 정상적인 이메일 변경까지 어렵게 만들었습니다.
이번에는 회원의 정체성은 members.id가 맡고, 이메일의 중복 금지는 UNIQUE 제약이 맡도록 책임을 분리합니다.
대리 키를 도입한다는 말은 업무 규칙을 버린다는 뜻이 아닙니다.
대표 식별자는 안정적인 값으로 고정하고, 자연스럽게 발생한 후보 키는 대체 키로 계속 보호한다는 뜻입니다.
자연 키와 대리 키
자연 키는 이메일, ISBN, 국가 코드처럼 업무 세계에 이미 존재하는 값입니다.
값을 보면 의미를 알 수 있고, 조건이 맞으면 추가 조인 없이 업무 대상을 바로 찾을 수 있습니다.
반면 정책 변경, 형식 변경, 재발급, 개인정보 취급 같은 업무 변화가 식별 체계까지 흔들 수 있습니다.
대리 키는 행을 식별하려고 데이터베이스 안에서 부여한 값입니다.
보통 숫자나 UUID이며 업무 의미를 넣지 않습니다.
값이 작고 안정적이어서 참조하기 쉽지만, 대리 키만 추가하면 이메일 중복 같은 업무 규칙을 놓치기 쉽습니다.
| 판단 축 | 자연 키를 PK로 사용 | 대리 키를 PK로 사용 |
|---|---|---|
| 값의 의미 | 업무 의미가 있음 | 의미가 없음 |
| 변경 영향 | 업무 변경이 참조에 전파될 수 있음 | 업무 속성을 독립적으로 변경 가능 |
| 중복 방지 | PK가 자연스럽게 막음 | 별도 UNIQUE가 반드시 필요 |
| 참조 크기 | 문자열·복합 값이면 넓어질 수 있음 | 짧은 숫자로 단순화 가능 |
| 외부 노출 | 업무 값이 곧 식별자라 결합이 큼 | 공개 ID 정책을 따로 설계 가능 |
둘 중 하나가 언제나 정답은 아닙니다.
범위가 작고 변경 관리 주체가 분명한 코드 테이블은 자연 키가 간결할 수 있습니다.
반면 회원, 게시글, 댓글처럼 수명이 길고 여러 테이블과 시스템이 참조하는 엔티티는 대리 키를 대표 식별자로 두는 편이 변화에 강합니다.
안정적인 회원 식별자
이제 오류 재현용 테이블과 분리된 실제 누적 모델을 만듭니다.
아래 스크립트는 MySQL 8.4와 기본 스토리지 엔진인 InnoDB를 기준으로 그대로 실행할 수 있습니다.
USE board_lab;
DROP TABLE IF EXISTS post_attachments;
DROP TABLE IF EXISTS attachments;
DROP TABLE IF EXISTS post_tags;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS comments;
DROP TABLE IF EXISTS member_profiles;
DROP TABLE IF EXISTS daily_post_quota;
DROP TABLE IF EXISTS posts;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS member_external_identities;
DROP TABLE IF EXISTS members;
CREATE TABLE members (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(80) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uq_members_email (email),
CONSTRAINT ck_members_status
CHECK (status IN ('ACTIVE', 'SUSPENDED', 'DELETED'))
) ENGINE = InnoDB;
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
author_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(160) NOT NULL,
content TEXT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
view_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL
DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6),
published_at DATETIME(6) NULL,
version BIGINT UNSIGNED NOT NULL DEFAULT 0,
request_key VARCHAR(64) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_posts_author_request (author_id, request_key),
KEY ix_posts_author_created (author_id, created_at DESC, id DESC),
CONSTRAINT ck_posts_status
CHECK (status IN ('DRAFT', 'HIDDEN', 'PUBLISHED', 'DELETED')),
CONSTRAINT fk_posts_author
FOREIGN KEY (author_id)
REFERENCES members (id)
) ENGINE = InnoDB;AUTO_INCREMENT는 MySQL 전용 생성 전략입니다.
대리 키라는 모델링 개념은 DBMS에 독립적이지만, PostgreSQL의 식별자, SQL 서버의 IDENTITY, 애플리케이션에서 만드는 UUID처럼 값을 발급하는 문법은 제품마다 다릅니다.
이 교재에서는 한 MySQL 인스턴스에서 시작하는 회원 게시판에 맞춰 BIGINT UNSIGNED AUTO_INCREMENT를 기준으로 사용합니다.
uq_members_email을 빼면 대리 키가 다른 두 행을 만들어 주기 때문에 같은 이메일 회원을 여러 번 저장할 수 있습니다.
대리 키는 행의 유일성을 보장할 뿐, 어떤 업무 속성이 중복되면 안 되는지는 알지 못합니다.
식별자는 고정하고 이메일만 바꾼다
INSERT INTO members (email, password_hash, name)
VALUES ('min@old.example', '$2a$12$example.hash', '안동민');
SET @member_id := LAST_INSERT_ID();
INSERT INTO posts
(author_id, title, content, status, view_count, created_at, published_at, request_key)
VALUES
(@member_id, '대리 키의 책임', '대표 식별자와 업무 키를 분리합니다.', 'PUBLISHED', 50,
'2026-07-13 13:00:00.000000', '2026-07-13 13:00:00.000000', 'post-key-001'),
(@member_id, '업무 키의 유일성', '이메일 중복은 별도 제약으로 막습니다.', 'PUBLISHED', 40,
'2026-07-13 14:10:00.000000', '2026-07-13 14:10:00.000000', 'post-key-002');
UPDATE members
SET email = 'min@new.example'
WHERE id = @member_id;
SELECT
l.id AS member_id,
l.email,
s.id AS post_id,
s.title
FROM members AS l
JOIN posts AS s
ON s.author_id = l.id
WHERE l.id = @member_id
ORDER BY s.id;예상 결과에서 회원 이메일은 min@new.example로 바뀌지만 두 게시글의 author_id는 그대로입니다.
변경은 업무 속성 한 행에서 끝나고, 게시글의 소유 관계는 흔들리지 않습니다.
member_id | email | post_id | title
1 | min@new.example | 1 | 대리 키의 책임
1 | min@new.example | 2 | 업무 키의 유일성반대로 같은 이메일을 다시 넣으면 다음 제약이 작동합니다.
INSERT INTO members (email, password_hash, name)
VALUES ('min@new.example', '$2a$12$duplicate.hash', '중복 사용자');ERROR 1062 (23000): Duplicate entry 'min@new.example'
for key 'members.uq_members_email'이 오류는 설계 의도입니다.
members.id는 참조 안정성을, uq_members_email은 이메일 유일성을 각각 지킵니다.
외부 로그인 식별자
한 사용자가 GitHub와 Google 계정을 모두 연결할 수 있다고 해 봅시다.
공급자가 준 식별자를 members의 기본 키로 쓰면 공급자 정책과 내부 회원 수명이 결합됩니다.
외부 계정을 독립된 자식 테이블로 두면 내부 회원 하나에 여러 계정을 연결할 수 있습니다.
CREATE TABLE member_external_identities (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
member_id BIGINT UNSIGNED NOT NULL,
provider VARCHAR(30) NOT NULL,
provider_subject VARCHAR(191) NOT NULL,
linked_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_member_external_identities_provider_subject
(provider, provider_subject),
UNIQUE KEY uk_member_external_identities_member_provider
(member_id, provider),
KEY ix_member_external_identities_member (member_id),
CONSTRAINT ck_member_external_identities_provider
CHECK (provider IN ('github', 'google')),
CONSTRAINT fk_member_external_identities_member
FOREIGN KEY (member_id)
REFERENCES members (id)
) ENGINE = InnoDB;
INSERT INTO member_external_identities
(member_id, provider, provider_subject)
VALUES
(@member_id, 'github', 'github-user-9182'),
(@member_id, 'google', 'google-user-4410');첫 번째 복합 UNIQUE는 같은 공급자의 같은 외부 계정이 두 내부 회원에게 연결되는 일을 막습니다.
두 번째는 한 내부 회원이 같은 공급자 계정을 여러 개 연결하지 못하게 하는 현재 정책입니다.
나중에 “GitHub 계정을 여러 개 연결할 수 있다”로 요구가 바뀌면 두 번째 제약만 재검토하면 됩니다.
대표 식별자는 그대로입니다.
INSERT INTO member_external_identities
(member_id, provider, provider_subject)
VALUES
(@member_id, 'github', 'github-user-9182');이 문장은 uk_member_external_identities_provider_subject 또는 uk_member_external_identities_member_provider 중 먼저 검사된 제약에서 오류 1062를 냅니다.
어느 제약에서 오류가 발생하든 “동일 계정을 중복 연결하지 않는다”는 결과는 같지만, 두 제약이 표현하는 업무 문장은 다르므로 이름을 분리해 두었습니다.
대리 키 선택 기준
AUTO_INCREMENT는 짧고 정렬 친화적이며 사람이 로그를 읽기 쉽습니다.
단일 쓰기 원점에서 시작하는 서비스에는 좋은 기본값입니다.
다만 여러 지역에서 오프라인으로 ID를 발급해야 하거나, 데이터를 합칠 때 충돌 없는 값이 필요하다면 UUID나 별도의 분산 ID 생성 전략을 검토할 수 있습니다.
선택할 때는 다음을 구분해야 합니다.
- 저장용 기본 키: 조인과 외래 키에 적합한 안정적인 값
- 공개 식별자: URL이나 API에 노출할 값, 추측 가능성 정책 포함
- 업무 키: 이메일, 외부 계정처럼 중복되면 안 되는 값
AUTO_INCREMENT 값을 감춘다고 보안이 생기지는 않으며, 노출한다고 권한 검사를 생략할 수도 없습니다.
공개 ID가 순차적이라는 사실과 “이 사용자가 해당 행을 읽을 권한이 있는가”는 별개의 문제입니다.
InnoDB에서는 각 보조 인덱스 레코드에 기본 키 값이 함께 저장됩니다.
따라서 매우 긴 문자열이나 넓은 복합 키를 기본 키로 고르면 보조 인덱스도 커질 수 있습니다.
작은 숫자 대리 키가 유리한 이유 중 하나입니다.
그렇다고 자연 키 조회가 무조건 느린 것은 아닙니다.
UNIQUE (email) 인덱스가 있으면 이메일 검색은 여전히 인덱스를 사용할 수 있고, 실제 선택은 EXPLAIN ANALYZE와 데이터 분포로 확인해야 합니다.
연습 문제
회원 게시판에 첨부파일의 출처를 저장하는 attachments를 추가하려 합니다.
첨부파일에는 URL이 있지만, 운영자가 URL을 정규화하거나 도메인이 이전될 수 있습니다.
같은 정규화 URL은 중복 저장하지 않아야 합니다.
- URL을 기본 키로 쓰지 않는 이유를 설명하세요.
- 대리 키와 업무 키를 분리한 DDL을 작성하세요.
- URL이 바뀌어도 기존 참조가 유지되는지 확인하는 SQL을 작성하세요.
해설과 예시 답안
URL은 사람이 이해할 수 있는 자연 키 후보지만 리다이렉트, 쿼리 정규화, 도메인 이전으로 바뀔 수 있습니다.
내부 참조는 대리 키로 고정하고 정규화 URL의 중복은 대체 키로 막습니다.
CREATE TABLE attachments (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
canonical_url VARCHAR(768) NOT NULL,
title VARCHAR(200) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_attachments_canonical_url (canonical_url)
) ENGINE = InnoDB;
INSERT INTO attachments (canonical_url, title)
VALUES ('https://dev.mysql.com/doc/refman/8.4/en/', 'MySQL 8.4 Reference Manual');
SET @attachment_id := LAST_INSERT_ID();
UPDATE attachments
SET canonical_url = 'https://dev.mysql.com/doc/refman/8.4/en/index.html'
WHERE id = @attachment_id;
SELECT id AS attachment_id, canonical_url
FROM attachments
WHERE id = @attachment_id;실무에서는 긴 URL 전체의 인덱스 크기, 정규화 규칙, 해시 충돌 처리도 함께 결정해야 합니다.
여기서는 키 책임의 분리에 집중한 최소 모델입니다.
핵심 정리
- 자연 키는 업무 의미와 유일성을 가지지만 변경 정책까지 함께 따라옵니다.
- 대리 키는 안정적인 참조점을 만들지만 업무 중복을 자동으로 막지 않습니다.
- 회원 게시판은
members.id를 PK로, 이메일을UNIQUE NOT NULL대체 키로 사용합니다. - 외부 로그인 계정은 별도 테이블과 복합 유일성 제약으로 내부 회원 수명에서 분리합니다.
- MySQL의
AUTO_INCREMENT는 구현 전략이며, 대리 키 자체는 제품에 독립적인 모델링 선택입니다.
다음 문서에서는 한 컬럼으로 끝나지 않는 복합 키를 다루고, members와 카테고리·게시글 사이의 일대다 관계를 완성합니다.