본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
10장 : 키와 관계 모델

키의 종류와 역할

회원 게시판의 이메일 자연 키 오류를 통해 후보 키, 기본 키, 대체 키, 외래 키의 책임을 구분합니다.

회원 게시판에서 사용자가 이메일을 바꾸려는 순간을 생각해 봅시다.

이메일을 회원의 기본 키로 삼았다면 변경 한 번이 게시글, 댓글, 태그, 감사 기록까지 번집니다.

반면 내부 식별자와 이메일 규칙을 분리했다면 회원을 가리키는 값은 그대로 두고 로그인 주소만 바꿀 수 있습니다.

키 설계의 출발점은 “어떤 컬럼이 유일한가?”가 아닙니다.

한 행의 정체성을 무엇으로 고정하고, 어떤 업무 규칙을 별도의 제약으로 보존할 것인가를 결정하는 일입니다.

1장에서 사용한 PK와 FK는 각각 대표 행과 테이블 사이 참조를 맡았습니다.

여기서는 한 단계 더 들어가 ‘유일하게 보이는 조합’에서 불필요한 열을 빼도 유일한지 확인합니다.

최소한의 유일 조합이 후보 키이고, 그중 대표로 고른 것이 기본 키, 고르지 않았지만 UNIQUE로 보존한 것이 대체 키입니다.

이 관계를 먼저 잡으면 아래 용어표가 서로 떨어진 암기 목록이 아니라 한 선택 과정으로 읽힙니다.


키의 역할

키는 하나 이상의 컬럼으로 행을 구별하는 값의 집합입니다.

모두 “유일성”과 관련되어 보이지만 역할은 다릅니다.

종류질문회원 게시판 예시
슈퍼 키이 조합으로 행을 구별할 수 있는가?(email, name)
후보 키유일하면서 불필요한 컬럼이 없는가?email, (provider, provider_subject)
기본 키후보 중 행의 대표 정체성으로 무엇을 고를까?members.id
대체 키기본 키로 고르지 않은 후보를 어떻게 보존할까?UNIQUE (email)
외래 키다른 테이블의 어느 행과 연결되는가?posts.author_id

후보 키의 핵심은 유일성최소성입니다.

이메일이 이미 모든 사용자를 구별한다면 (email, name)도 결과적으로는 유일합니다.

하지만 name을 빼도 유일하므로 최소성이 없습니다.

이런 조합은 슈퍼 키일 수는 있어도 후보 키는 아닙니다.

기본 키는 다음 성질을 갖습니다.

  • NULL일 수 없습니다.
  • 테이블 안에서 중복될 수 없습니다.
  • 다른 테이블이 오래 참조할 수 있도록 값이 안정적이어야 합니다.

세 번째는 문법상 강제되는 조건이 아니라 설계 원칙입니다.

MySQL은 참조 관계와 옵션이 허용하는 범위에서 기본 키 값을 수정할 수 있지만, “수정할 수 있다”와 “수정해도 좋은 식별자다”는 전혀 다른 말입니다.

UNIQUE만 붙이면 후보 키가 될까?

아닙니다.

업무상 반드시 값이 있어야 하는 후보 키라면 NOT NULL도 함께 검토해야 합니다.

MySQL의 UNIQUE 인덱스는 NULL을 서로 같은 값으로 취급하지 않으므로 여러 행에 NULL이 들어갈 수 있습니다.

유일하지만 비어 있을 수 있는 컬럼
CREATE TEMPORARY TABLE candidate_key_demo (
  demo_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  external_code VARCHAR(40) NULL,
  PRIMARY KEY (demo_id),
  UNIQUE KEY uk_candidate_demo_external_code (external_code)
);

INSERT INTO candidate_key_demo (external_code)
VALUES (NULL), (NULL), ('EXT-100');

SELECT demo_id, external_code
FROM candidate_key_demo
ORDER BY demo_id;

예상 결과에서 NULL인 행은 두 개 모두 저장됩니다.

외부 코드가 모든 행을 식별해야 한다면 컬럼을 NOT NULL로 바꾸거나, 외부 코드가 없는 상태를 허용하는 별도 업무 규칙을 명시해야 합니다.

제약 이름도 설계의 일부입니다.

uk_테이블_업무규칙, fk_자식_부모처럼 역할이 드러나는 이름을 붙이면 운영 오류에서 어느 규칙이 깨졌는지 바로 찾을 수 있습니다.

이름을 생략하면 MySQL이 자동 이름을 만들 수 있지만, 스키마 비교와 마이그레이션에서 의도를 읽기 어려워집니다.

키를 “컬럼 옆 장식”으로 보지 않고 이름 있는 업무 규칙으로 다루는 습관이 이후 모델 변경 비용을 낮춥니다.


잘못된 이메일 자연 키

먼저 이메일을 회원의 자연 키로 사용한 설계를 일부러 만들어 보겠습니다.

이 테이블은 오류를 관찰하기 위한 연습용이며 다음 문서에서 올바른 모델로 교체합니다.

01_natural_key_failure.sql
CREATE DATABASE IF NOT EXISTS board_lab
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_0900_ai_ci;

USE board_lab;

DROP TABLE IF EXISTS posts_natural;
DROP TABLE IF EXISTS members_natural;

CREATE TABLE members_natural (
  email VARCHAR(255) NOT NULL,
  name VARCHAR(80) NOT NULL,
  joined_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  PRIMARY KEY (email)
) ENGINE = InnoDB;

CREATE TABLE posts_natural (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  author_email VARCHAR(255) NOT NULL,
  title VARCHAR(160) NOT NULL,
  view_count SMALLINT UNSIGNED NOT NULL,
  created_at DATETIME(6) NOT NULL,
  PRIMARY KEY (id),
  KEY ix_posts_natural_author (author_email),
  CONSTRAINT fk_posts_natural_author
    FOREIGN KEY (author_email)
    REFERENCES members_natural (email)
) ENGINE = InnoDB;

INSERT INTO members_natural (email, name)
VALUES ('min@old.example', '안동민');

INSERT INTO posts_natural
  (author_email, title, view_count, created_at)
VALUES
  ('min@old.example', '후보 키와 최소성', 45, '2026-07-13 09:00:00.000000'),
  ('min@old.example', '외래 키 실습', 35, '2026-07-13 10:00:00.000000');

이 모델은 당장은 그럴듯합니다.

이메일은 중복되지 않고, 게시글이 존재하지 않는 회원을 가리키는 일도 외래 키가 막아 줍니다.

문제는 이메일이 로그인 정책, 회사 도메인, 사용자의 선택에 따라 변하는 업무 속성이라는 데 있습니다.

이메일 변경 시도
UPDATE members_natural
SET email = 'min@new.example'
WHERE email = 'min@old.example';

MySQL 8.4의 InnoDB에서 기본 동작인 RESTRICT에 의해 다음과 같은 오류가 발생합니다.

제약 이름과 출력 형식은 환경에 따라 조금 달라도 오류 코드는 같은 의미입니다.

ERROR 1451 (23000): Cannot delete or update a parent row:
a foreign key constraint fails (... fk_posts_natural_author ...)

오류는 외래 키가 잘못된 것이 아니라 외래 키가 잘 작동한 결과입니다.

부모 식별자를 먼저 바꾸면 자식 행은 더 이상 존재하는 회원을 가리킬 수 없으므로 변경을 거절한 것입니다.

ON UPDATE CASCADE면 해결될까?

연쇄 갱신을 지정하면 문법적으로는 이메일 변경을 성공시킬 수 있습니다.

하지만 참조 테이블이 늘어날수록 한 번의 프로필 수정이 대량 갱신이 되고, 외부 시스템에 이미 전송한 식별자는 자동으로 바뀌지 않습니다.

과거 게시글에 당시 이메일을 남겨야 하는 요구와 현재 이메일을 가리켜야 하는 요구도 뒤섞입니다.

CASCADE는 유효한 참조 동작이지, 변하는 업무 값을 영구 식별자로 고른 판단 자체를 고쳐 주는 장치는 아닙니다.

참조 대상의 생명주기와 변경 파급을 이해한 뒤 선택해야 합니다.

연쇄 갱신의 파급 범위 확인
SELECT
  rc.CONSTRAINT_NAME,
  rc.TABLE_NAME,
  rc.REFERENCED_TABLE_NAME,
  rc.UPDATE_RULE
FROM information_schema.REFERENTIAL_CONSTRAINTS AS rc
WHERE rc.CONSTRAINT_SCHEMA = 'board_lab'
  AND rc.REFERENCED_TABLE_NAME = 'members_natural';

이 조회는 MySQL의 information_schema를 사용하는 진단 예제입니다.

PRIMARY KEY, UNIQUE, FOREIGN KEY라는 개념은 관계형 모델과 SQL 전반에 통하지만, 메타데이터 뷰의 컬럼과 표현은 DBMS마다 다릅니다.


식별자와 업무 규칙

이메일에는 두 책임이 섞여 있습니다.

  1. 사용자를 찾아 로그인하게 하는 업무 키
  2. 다른 테이블이 사용자를 가리키는 참조 식별자

업무 키는 정책에 따라 바뀔 수 있고, 대표 식별자는 참조가 살아 있는 동안 안정적이어야 합니다.

따라서 다음 문서에서는 의미 없는 members.id를 기본 키로 두고, 이메일에는 UNIQUE NOT NULL을 둡니다.

그러면 이메일 중복은 계속 막으면서 참조 식별자는 바뀌지 않습니다.

키 선택을 할 때는 다음 순서가 안전합니다.

  1. 행을 유일하게 만드는 후보를 모두 적습니다.
  2. 후보마다 값이 비는지, 바뀌는지, 재사용되는지 확인합니다.
  3. 다른 시스템과 테이블이 얼마나 오래 참조할지 확인합니다.
  4. 대표 식별자와 업무상 유일성 규칙을 분리할지 결정합니다.
  5. 선택하지 않은 후보도 필요한 경우 UNIQUE 제약으로 보존합니다.

자연 키가 항상 나쁜 것은 아닙니다.

ISO 국가 코드처럼 범위가 작고, 공식 관리 주체가 있으며, 변경과 재사용 정책이 명확한 값은 자연 키로도 적합할 수 있습니다.

다만 “현재 중복이 없다”만으로는 부족합니다.

변경 가능성과 참조 수명을 확인해야 합니다.


연습 문제

회원 게시판이 소셜 로그인을 지원한다고 가정해 봅시다.

로그인 공급자는 google, github 중 하나이고, 각 공급자는 provider_subject라는 사용자 식별자를 줍니다.

다음 중 후보 키를 고르고, 기본 키로 쓸지 대체 키로 둘지 판단해 보세요.

  • provider
  • provider_subject
  • (provider, provider_subject)
  • (provider, provider_subject, name)

또한 한 사용자가 여러 로그인 공급자를 연결할 수 있다는 요구까지 고려해 테이블의 최소 컬럼을 적어 보세요.

해설과 예시 답안

provider_subject가 공급자마다만 유일하다면 단독으로는 후보 키가 아닙니다.

(provider, provider_subject)는 두 값을 함께 사용해야 한 외부 계정을 유일하게 찾을 수 있으므로 복합 후보 키입니다.

여기에 name을 더한 조합은 유일하더라도 최소성이 없어 후보 키가 아닙니다.

외부 공급자의 정책에 식별을 맡기기보다 내부 대리 키를 기본 키로 두고 복합 후보 키를 대체 키로 보존할 수 있습니다.

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,
  PRIMARY KEY (id),
  UNIQUE KEY uk_member_external_identities_provider_subject
    (provider, provider_subject)
);

아직 members 최종 테이블을 만들지 않았으므로 외래 키는 다음 문서에서 추가합니다.

여기서 핵심 판단은 대표 기본 키와 외부 공급자의 유일성 규칙을 같은 컬럼에 맡기지 않았다는 점입니다.


핵심 정리

  • 후보 키는 유일성과 최소성을 모두 만족합니다.
  • 기본 키는 대표 정체성, 대체 키는 선택되지 않은 업무상 유일성을 맡습니다.
  • 외래 키는 다른 테이블의 행을 가리키고 참조 무결성을 지킵니다.
  • 변하는 자연 키를 기본 키로 쓰면 변경 파급, 기록 해석, 외부 연동 문제가 커집니다.
  • 안정적인 내부 식별자와 UNIQUE 업무 규칙을 분리하면 두 책임을 함께 보존할 수 있습니다.

다음 문서에서는 오류 재현용 자연 키 모델을 떠나, 회원 게시판의 실제 누적 모델을 대리 키 중심으로 다시 만듭니다.