본문 바로가기

공통 코드 테이블

같은 상태가 여러 철자로 저장되어 집계가 갈라지는 오류를 재현하고, 그룹과 상세 코드, 복합 외래 키로 회원 게시판의 입력 규칙을 고정합니다. 정렬 규칙과 활성 플래그가 보장하지 않는 것도 함께 확인합니다.

13장부터 18장까지는 핵심 과정을 마친 뒤 필요한 주제를 골라 읽는 선택 심화입니다. 이 장은 상태와 유형 같은 값의 표시 이름과 활성 여부, 배포의 수명을 데이터로 운영해야 할 때 읽습니다. 10장의 복합 키와 외래 키, 12장의 최종 DDL을 이해했다면 시작할 수 있습니다.

상태값은 짧지만 행 전체의 뜻을 바꿉니다. PUBLISHED와 게시, published가 모두 같은 뜻이더라도, 비교에 쓰는 정렬 규칙과 입력된 철자에 따라 집계와 상태 전이가 읽는 행이 달라집니다.

공통 코드(common code)는 모든 문자열을 한 표에 모으는 기술이 아닙니다. 값이 유일한 범위와 표시 이름, 사용 여부를 나누고, 실제 업무 행이 올바른 그룹의 코드만 참조하게 만드는 데이터의 규칙입니다. 예제는 회원 게시판의 가져오기 경로가 게시글의 상태와 공개 범위를 자유 문자열로 받는다고 가정하고, 같은 PUBLISHED의 뜻을 세 가지 철자로 저장한 뒤 집계에서 빠지는 행을 확인합니다.

문자열 상태값의 불일치

오타는 SQL의 문법 오류가 아니므로 INSERT는 모두 성공합니다. 아래 테이블은 상태와 공개 범위를 아무 문자열이나 받는 VARCHAR(30)으로 두고, 같은 게시 상태를 PUBLISHED, published, 게시의 세 철자로 넣습니다. 마지막 SELECT는 애플리케이션이 흔히 쓰는 post_status = 'PUBLISHED' 조건으로 게시글 수와 조회수를 집계합니다.

검증 없는 자유 문자열 게시글
DROP TABLE IF EXISTS post_code_bad;

CREATE TABLE post_code_bad (
  post_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  post_status VARCHAR(30) NOT NULL,
  visibility VARCHAR(30) NOT NULL,
  view_count SMALLINT UNSIGNED NOT NULL,
  PRIMARY KEY (post_id)
);

INSERT INTO post_code_bad
  (post_status, visibility, view_count)
VALUES
  ('PUBLISHED', 'PUBLIC', 30),
  ('published', 'manual', 40),
  ('게시', '직접입력', 50);

SELECT COUNT(*) AS published_count,
       SUM(view_count) AS published_views
FROM post_code_bad
WHERE post_status = 'PUBLISHED';
published_count | published_views
2               | 70

세 행이 오류 없이 들어갔습니다. 이 교재의 기준 정렬 규칙 utf8mb4_0900_ai_ci는 대소문자를 구별하지 않으므로 PUBLISHED와 published가 같게 비교되어 두 행, 조회수 70이 집계되고, 게시 행은 빠집니다. 대소문자를 구별하는 정렬 규칙이었다면 한 행, 30만 집계되었을 것입니다.

애플리케이션의 상수만으로는 관리자 SQL과 가져오기, 오래된 서비스가 같은 허용 집합을 쓴다고 보장할 수 없습니다. 반대로 표시 이름을 업무 행에 저장하면, 번역이나 문구를 바꾸는 일이 사실 데이터의 대량 수정으로 번집니다. 코드값은 안정적인 식별자이고 표시 이름은 바뀔 수 있는 설명이므로 두 역할을 나누고, 그룹을 키에 포함해 ACTIVE처럼 여러 문맥에 있을 수 있는 값도 충돌하지 않게 합니다.

설계에 앞서 네 가지를 정합니다. API와 배치, 관리 SQL이 같은 허용 집합을 거치는지 입력 경로를 나열하고, 코드 하나가 전역에서 유일한지 그룹 안에서만 유일한지 정합니다. 저장할 식별자와 사용자에게 보일 문구는 다른 열로 두며, 대소문자와 공백, 번역 문자열을 넣어 어느 제약에서 막히는지 반례로 확인합니다.

코드 그룹과 허용 값

(group_code, code)의 복합 키는 값이 뜻을 갖는 범위를 나타냅니다. 업무 테이블도 두 열을 함께 참조해야 다른 그룹의 같은 철자를 잘못 끌어오지 않습니다.

회원의 ('MEMBER_STATUS', 'DELETED')는 탈퇴이고, 게시글의 ('POST_STATUS', 'DELETED')는 삭제입니다. 두 행은 복합 키가 다르므로 함께 있을 수 있지만, code = 'DELETED'만으로 JOIN하면 어느 문맥의 표시 이름을 원하는지 정하지 못합니다. 뒤의 게시글 상태 JOIN이 그룹과 코드의 두 조건을 쓰는 이유입니다.

코드를 지우면 이미 저장된 사실과의 연결이 끊길 수 있으므로, 보통은 is_active로 새로운 선택만 막습니다. 이미 저장된 행은 계속 해석할 수 있어야 하고, 표시 이름의 변경은 식별자의 변경과 분리합니다. 코드값은 배포와 로그, API가 오래 참조할 수 있는 영문 식별자로 둡니다.

CODE NAMESPACE · COMPOSITE FK

업무 행은 그룹과 코드를 함께 참조한다

POST_STATUS의 PUBLISHED와 POST_VISIBILITY의 PUBLIC은 각 그룹 안에서 해석합니다. 두 열의 복합 FK가 코드 존재를, 그룹 CHECK가 업무 이름 공간을 검사합니다.

그룹과상태·공개범위의세외래키실제외래키화살표는자식에서부모키로향합니다. 상세코드는그룹한행을참조하고,게시글의상태와공개범위는각각별도의복합FK로상세코드한행을참조합니다. 부모한행에는0개이상의자식이올수있습니다. 두DELETED코드는그룹이다릅니다.N : 1상태 FK왼쪽 → 부모공개 FK오른쪽 → 부모final_code_groupPK: group_codefinal_codePK: group_code + codeMEMBER_STATUS+ DELETED (탈퇴)POST_STATUS+ DELETED (삭제)final_post_code_demo상태의 그룹 + 코드POST_STATUS+ PUBLISHED공개 범위의 그룹 + 코드POST_VISIBILITY+ PUBLIC
그룹과상태·공개범위의세외래키실제외래키화살표는자식에서부모키로향합니다. 상세코드는그룹한행을참조하고,게시글의상태와공개범위는각각별도의복합FK로상세코드한행을참조합니다. 부모한행에는0개이상의자식이올수있습니다. 두DELETED코드는그룹이다릅니다.N : 1상태 FK왼쪽 → 부모공개 FK오른쪽 → 부모final_code_groupPK: group_codefinal_codePK: group_code + codeMEMBER_STATUS+ DELETED (탈퇴)POST_STATUS+ DELETED (삭제)final_post_code_demo상태의 그룹 + 코드POST_STATUS+ PUBLISHED공개 범위의 그룹 + 코드POST_VISIBILITY+ PUBLIC

게시글의 두 FK는 각각 NOT NULL입니다. 그룹 CHECK는 상태와 공개 범위의 이름 공간을 고정하며, 코드의 활성 여부까지 검사하지는 않습니다.

화살표는 자식 FK → 부모 키입니다. 상세→그룹 한 개와 게시글→상세 두 개를 따로 읽습니다. 각 부모에는 자식이 0개 이상 연결될 수 있고, 각 NOT NULL FK는 부모 한 행을 참조합니다.

게시글 한 행 · 두 복합 참조

상태: (POST_STATUS, PUBLISHED) → final_code의 같은 쌍

공개 범위: (POST_VISIBILITY, PUBLIC) → final_code의 같은 쌍

코드의 is_active는 이 FK로 검사하지 않습니다. 본문의 ai_ci 비교도 철자의 대소문자 정규화를 강제하지 않습니다.

공통 코드와 입력 제약

먼저 그룹과 상세 코드를 만들고, 게시글 데모 행에는 상태 그룹과 공개 범위 그룹을 명시합니다. 13장 2편부터 만드는 정책과 번역 테이블이 아직 없는 상태에서 시작하며, 이미 다른 자식 테이블이 이 코드를 참조한다면 아래의 DROP 순서만으로는 부모 테이블을 다시 만들 수 없습니다. CHECK가 그룹의 상수를 고정하고, 복합 외래 키가 실제 코드의 존재를 검증합니다.

회원 게시판 공통 코드 기준선
DROP TABLE IF EXISTS final_post_code_demo;
DROP TABLE IF EXISTS final_code;
DROP TABLE IF EXISTS final_code_group;

CREATE TABLE final_code_group (
  group_code VARCHAR(40) NOT NULL,
  group_name VARCHAR(100) NOT NULL,
  description VARCHAR(300) NOT NULL,
  PRIMARY KEY (group_code)
) ENGINE=InnoDB;

CREATE TABLE final_code (
  group_code VARCHAR(40) NOT NULL,
  code VARCHAR(40) NOT NULL,
  display_name_ko VARCHAR(100) NOT NULL,
  sort_order SMALLINT UNSIGNED NOT NULL,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  PRIMARY KEY (group_code, code),
  CONSTRAINT fk_final_code_group FOREIGN KEY (group_code)
    REFERENCES final_code_group (group_code) ON DELETE RESTRICT,
  CONSTRAINT uq_final_code_order UNIQUE (group_code, sort_order),
  CONSTRAINT chk_final_code_active CHECK (is_active IN (0, 1))
) ENGINE=InnoDB;

INSERT INTO final_code_group
  (group_code, group_name, description)
VALUES
  ('MEMBER_STATUS', '회원 상태', '가입·정지·탈퇴 수명주기'),
  ('POST_STATUS', '게시글 상태', '초안·게시·숨김·삭제 수명주기'),
  ('POST_VISIBILITY', '게시글 공개 범위', '전체·회원·작성자 공개 범위');

INSERT INTO final_code
  (group_code, code, display_name_ko, sort_order)
VALUES
  ('MEMBER_STATUS', 'ACTIVE', '활성', 10),
  ('MEMBER_STATUS', 'SUSPENDED', '정지', 20),
  ('MEMBER_STATUS', 'DELETED', '탈퇴', 30),
  ('POST_STATUS', 'DRAFT', '초안', 10),
  ('POST_STATUS', 'PUBLISHED', '게시됨', 20),
  ('POST_STATUS', 'HIDDEN', '숨김', 30),
  ('POST_STATUS', 'DELETED', '삭제됨', 40),
  ('POST_VISIBILITY', 'PUBLIC', '전체 공개', 10),
  ('POST_VISIBILITY', 'MEMBERS_ONLY', '회원 공개', 20),
  ('POST_VISIBILITY', 'PRIVATE', '작성자 전용', 30);

CREATE TABLE final_post_code_demo (
  post_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  status_group_code VARCHAR(40) NOT NULL DEFAULT 'POST_STATUS',
  post_status VARCHAR(40) NOT NULL,
  visibility_group_code VARCHAR(40) NOT NULL DEFAULT 'POST_VISIBILITY',
  visibility_code VARCHAR(40) NOT NULL,
  view_count SMALLINT UNSIGNED NOT NULL,
  PRIMARY KEY (post_id),
  CONSTRAINT chk_demo_status_group
    CHECK (status_group_code = 'POST_STATUS'),
  CONSTRAINT chk_demo_visibility_group
    CHECK (visibility_group_code = 'POST_VISIBILITY'),
  CONSTRAINT chk_demo_view_count
    CHECK (view_count BETWEEN 0 AND 65535),
  CONSTRAINT fk_demo_status FOREIGN KEY
    (status_group_code, post_status)
    REFERENCES final_code (group_code, code),
  CONSTRAINT fk_demo_visibility FOREIGN KEY
    (visibility_group_code, visibility_code)
    REFERENCES final_code (group_code, code)
) ENGINE=InnoDB;

INSERT INTO final_post_code_demo
  (post_status, visibility_code, view_count)
VALUES ('PUBLISHED', 'PUBLIC', 30);

SELECT
  (SELECT COUNT(*) FROM final_code_group) AS code_groups,
  (SELECT COUNT(*) FROM final_code) AS detail_codes,
  (SELECT COUNT(*) FROM final_post_code_demo) AS post_rows;
code_groups | detail_codes | post_rows
3           | 10           | 1

업무 행은 안정적인 코드만 저장하고, 화면은 코드 테이블에서 한국어 이름을 읽습니다. 그룹 열을 반복해 저장하는 비용은 있지만, 외래 키가 뜻의 범위를 직접 보장합니다. 전역 대리 키 하나만 저장하면 숫자만으로는 그룹을 알 수 없고, 배포 사이에 ID를 고정하는 규칙도 따로 필요합니다.

is_active = 0은 과거의 참조를 허용하면서 새 입력을 막자는 정책의 신호일 뿐, 외래 키가 그 값을 저절로 막지는 않습니다. 쓰기 API가 활성 여부를 확인하고, 데이터베이스에는 과거의 코드가 모두 남아 있어야 합니다.

코드값과 표시 이름의 검증

오타는 외래 키가, 그룹의 혼동은 CHECK가 막습니다. 두 위반 INSERT는 각각 따로 실행합니다.

코드 규칙 위반과 표시 조회
INSERT INTO final_post_code_demo
  (post_status, visibility_code, view_count)
VALUES ('PUBLISH', 'PUBLIC', 20);

INSERT INTO final_post_code_demo
  (status_group_code, post_status, visibility_code, view_count)
VALUES ('POST_VISIBILITY', 'PUBLIC', 'PRIVATE', 20);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`board_lab`.`final_post_code_demo`, CONSTRAINT `fk_demo_status` ...)
ERROR 3819 (HY000): Check constraint 'chk_demo_status_group' is violated.

첫 문장은 그룹이 기본값 POST_STATUS로 맞지만 PUBLISH라는 코드가 없어 외래 키에서 거부됩니다. 둘째 문장의 ('POST_VISIBILITY', 'PUBLIC')은 코드 테이블에 실제로 있는 키이므로 외래 키만으로는 막을 수 없고, 상태 위치의 그룹을 고정한 CHECK가 거부합니다. 두 문장 모두 행을 남기지 않습니다.

표시 이름은 코드 테이블에서만 바꿉니다. 아래는 트랜잭션 안에서 이름을 바꾸어 조회한 뒤 ROLLBACK으로 되돌리므로, 뒤의 글에서는 원래의 이름 '게시됨'을 그대로 씁니다.

표시 이름의 변경과 조회
START TRANSACTION;

UPDATE final_code
SET display_name_ko = '공개됨'
WHERE group_code = 'POST_STATUS'
  AND code = 'PUBLISHED';

SELECT d.post_id,
       d.post_status,
       status_code.display_name_ko AS status_name,
       visibility_code.display_name_ko AS visibility_name,
       d.view_count
FROM final_post_code_demo AS d
JOIN final_code AS status_code
  ON status_code.group_code = d.status_group_code
 AND status_code.code = d.post_status
JOIN final_code AS visibility_code
  ON visibility_code.group_code = d.visibility_group_code
 AND visibility_code.code = d.visibility_code;

ROLLBACK;
post_id | post_status | status_name | visibility_name | view_count
1       | PUBLISHED   | 공개됨      | 전체 공개       | 30

UPDATE는 코드 테이블의 한 행만 바꾸었고, 업무 행의 post_status는 여전히 PUBLISHED입니다. 번역이나 문구를 바꾸어도 업무 행의 수와 집계에는 영향이 없습니다.

정렬 규칙과 활성 플래그가 보장하지 않는 것

코드 열의 정렬 규칙이 대소문자를 구별하지 않으면, 외래 키의 비교도 대소문자를 구별하지 않습니다. 반면 utf8mb4_0900_ai_ci는 끝 공백을 무시하지 않는 NO PAD 정렬 규칙이므로, 공백이 붙은 철자는 다른 값입니다. 트랜잭션 안에서 두 철자를 넣어 보고 되돌립니다.

대소문자와 끝 공백의 외래 키 비교
START TRANSACTION;

INSERT INTO final_post_code_demo
  (post_status, visibility_code, view_count)
VALUES ('published', 'PUBLIC', 10);

SELECT d.post_status, c.code AS parent_code
FROM final_post_code_demo AS d
JOIN final_code AS c
  ON c.group_code = d.status_group_code
 AND c.code = d.post_status
WHERE d.view_count = 10;

INSERT INTO final_post_code_demo
  (post_status, visibility_code, view_count)
VALUES ('PUBLISHED ', 'PUBLIC', 10);

ROLLBACK;
post_status | parent_code
published   | PUBLISHED

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`board_lab`.`final_post_code_demo`, CONSTRAINT `fk_demo_status` ...)

소문자 published는 부모의 PUBLISHED와 같다고 판단되어 저장되지만, 업무 행에는 소문자 철자 그대로 남습니다. 끝에 공백이 붙은 값은 부모 키가 없어 거부됩니다. API의 규칙에서 대소문자를 구별해야 한다면 코드 열과 그 코드를 참조하는 업무 행의 열에 함께 utf8mb4_0900_bin 같은 이진 규칙을 명시하거나, 저장하기 전에 입력을 대문자로 정규화합니다. 외래 키에서 참조하는 열과 참조되는 열은 문자 집합과 정렬 규칙이 같아야 하기 때문입니다. 같은 이진 규칙이라도 utf8mb4_bin은 끝 공백을 무시하는 PAD SPACE 규칙이어서, 이 규칙으로 바꾸면 방금 본 끝 공백의 거부가 사라집니다. ROLLBACK한 INSERT도 AUTO_INCREMENT 값을 쓰므로, 다음에 넣는 행의 post_id는 건너뛴 값이 됩니다.

코드 테이블을 모든 도메인 값을 모으는 곳으로 만들지도 않습니다. 외래 키와 수명, 추가 속성이 독립적인 엔티티라면 전용 테이블이 더 읽기 쉽습니다. 코드의 추가와 중단은 일반적인 데이터 수정이 아니라 호환성 변경이므로, 승인 기록과 영향받는 API 목록을 남기고, 외래 키 오류 1452와 비활성 코드의 입력 시도, 그룹별 코드 수, 겹친 sort_order를 운영 지표로 봅니다.

허용 집합을 저장하는 네 방식

방식장점비용알맞은 경우
CHECK 목록단순한 규칙을 데이터베이스 가까이 둔다이름과 정렬 같은 메타데이터가 부족하다값이 적고 배포와 함께 바뀔 때
전용 코드 테이블뜻과 전용 속성이 분명하다테이블 수가 늘어난다도메인마다 수명이 다를 때
그룹형 공통 코드여러 선택 목록을 일관되게 관리한다복합 외래 키와 운영 규칙이 필요하다작고 정적인 목록이 많을 때
자유 문자열처음 입력이 빠르다오타와 변경, 번역의 비용이 크다외부의 원문을 임시로 적재할 때

코드 자체의 이름을 바꾸면 과거의 로그와 이벤트, 외부의 규칙이 끊기므로, 새 코드를 만들고 별칭으로 전환합니다(13장 4편). 표시 순서는 사용자별 정렬과 다를 수 있으므로 업무의 뜻을 부여하지 않습니다. 테넌트마다 값이 다르다면 tenant_id를 키에 넣을지 전역 기준을 복제할지 정하고, 외부 공급자의 원문 상태는 임시 적재에 보존한 채 내부 코드로 바꾸지 못한 행을 격리합니다.

따져 볼 것질문
크기허용 집합이 작고 대부분 읽기인가
수명코드를 중단한 뒤에도 과거의 행을 해석해야 하는가
표시식별자와 이름, 번역을 나눌 필요가 있는가
무결성업무 행이 데이터베이스의 외래 키로 허용 집합을 참조해야 하는가
독립성별도의 엔티티로 승격할 만큼 관계와 속성이 많은가

공통 코드라는 이름보다 값이 유일한 범위와 수명을 먼저 설명합니다. 그룹형 테이블을 골랐다면 업무 행까지 복합 외래 키로 이어야, 중앙의 관리가 실제 무결성이 됩니다.

더 깊이: 열의 타입으로 허용 집합을 정하는 ENUM

MySQL에는 허용 값의 목록을 열의 타입으로 선언하는 ENUM도 있습니다. 목록의 값에는 정의한 순서대로 1부터 번호가 매겨지고 행에는 문자열 대신 이 번호가 저장되므로, MySQL 8.4 매뉴얼의 예에서 'medium' 100만 행은 VARCHAR로 600만 바이트가 들지만 ENUM으로는 100만 바이트면 됩니다. 한 열에는 서로 다른 값을 65,535개까지 둘 수 있습니다. 엄격 모드에서는 목록에 없는 값을 넣으면 오류가 나지만, 엄격 모드가 아니면 번호가 0인 빈 문자열이 오류 값으로 들어갑니다.

번호로 저장한다는 점이 함정이 되기도 합니다. ENUM 열의 정렬은 문자열의 사전 순서가 아니라 번호, 곧 정의한 순서를 따르므로 ENUM('b', 'a')에서는 'b'가 먼저 옵니다. 숫자처럼 보이는 값은 더 위험해서, ENUM('0','1','2') 열에 따옴표 없는 2를 넣으면 번호 2로 해석되어 '1'이 저장됩니다. 그래서 매뉴얼은 숫자를 ENUM의 값으로 쓰지 말라고 권합니다.

값을 더할 때의 비용도 위치에 따라 다릅니다. 목록의 끝에 새 값을 더하는 변경은 저장 크기가 바뀌지 않는 한 메타데이터만 바꾸는 INSTANT로 끝나지만, 가운데에 끼워 넣으면 기존 값의 번호가 다시 매겨지므로 테이블을 복사해야 합니다. 그리고 ENUM은 위 표의 CHECK 목록처럼 값이 바뀔 때마다 DDL이 필요하고, 표시 이름이나 활성 여부 같은 메타데이터를 담을 수 없습니다. 값이 적고 거의 바뀌지 않으며 정의한 순서가 곧 업무의 순서인 목록에서만 검토할 만한 선택입니다.

정리

  • 코드값과 표시 이름은 수명이 다르므로 다른 열과 테이블에 둡니다.
  • 그룹을 이름 공간으로 쓰면 상세 코드의 키는 (group_code, code)입니다.
  • 업무 행의 복합 외래 키가 중앙의 목록을 실제 허용 집합으로 만들고, 그룹을 고정하는 CHECK가 다른 그룹의 코드를 막습니다.
  • 대소문자를 구별하지 않는 정렬 규칙에서는 외래 키도 철자의 대소문자를 구별하지 않습니다.
  • 사용 중단은 과거의 참조를 끊는 삭제와 구분합니다.

연습 문제

1. 링크 공개 코드의 추가

기존 POST_VISIBILITY 그룹에 링크를 가진 사람만 읽는 LINK_ONLY 코드를 더하고, 게시글 데모에 연결해 보세요. POST_STATUS의 철자와 같은 값은 공개 범위 위치에 들어오지 않아야 합니다.

해설 보기

공개 범위 코드를 기존 그룹에 더하고, 복합 외래 키와 CHECK가 그룹의 범위를 지키는지 확인합니다.

INSERT INTO final_code
  (group_code, code, display_name_ko, sort_order)
VALUES ('POST_VISIBILITY', 'LINK_ONLY', '링크 보유자 공개', 40);

UPDATE final_post_code_demo
SET visibility_code = 'LINK_ONLY'
WHERE post_id = 1;

UPDATE final_post_code_demo
SET visibility_group_code = 'POST_STATUS',
    visibility_code = 'PUBLISHED'
WHERE post_id = 1;
ERROR 3819 (HY000): Check constraint 'chk_demo_visibility_group' is violated.

앞의 두 문장은 성공해 게시글 1의 공개 범위가 LINK_ONLY가 됩니다. 마지막 UPDATE의 ('POST_STATUS', 'PUBLISHED')는 실제로 있는 부모 키이므로 외래 키는 통과하지만, 공개 범위의 그룹을 고정한 CHECK에 걸립니다. 13장 2편은 이 연습으로 LINK_ONLY가 더해진 경우를 함께 다룹니다.

2. 비활성화한 코드

1번의 LINK_ONLY를 is_active = 0으로 비활성화했다고 합시다. 이미 이 코드를 참조하는 게시글을 계속 읽을 수 있나요? 같은 값을 SQL로 새 행에 직접 넣으면 외래 키가 거부하나요? 새 선택 목록에는 계속 보여 주어야 하나요?

해설 보기

기존의 참조는 그대로 읽을 수 있습니다. 부모 키가 남아 있으므로 외래 키는 새 INSERT도 거부하지 않습니다. 외래 키는 부모 행의 존재만 보고 is_active의 값은 보지 않기 때문입니다.

새 선택을 막는 일은 쓰기 API의 활성 여부 검사와 선택 목록의 필터가 맡습니다. 선택 목록은 WHERE is_active = 1로 걸러 새로 고르지 못하게 하고, 이미 저장된 행을 보여 줄 때는 비활성 코드의 이름도 읽을 수 있어야 합니다. 코드의 비활성화와 부모 행의 삭제를 같은 동작으로 생각하면 과거 데이터의 해석을 잃습니다.

다음 글 코드별 확장 속성에서는 코드마다 다른 정책 속성을 범용 슬롯이 아닌 명시적인 확장 관계로 설계합니다.