DB 코드와 애플리케이션 열거형
ENUM과 DB 코드가 서로 다른 값으로 배포되는 불일치를 재현하고 릴리스 목록·동기화 일치 검사·상태 전이 표로 책임을 분리합니다.
애플리케이션 ENUM은 타입 안전성과 자동 완성을 제공하고 DB 코드는 표시 이름과 운영 메타데이터를 즉시 바꿀 수 있습니다. 둘을 함께 쓰면 장점도 겹치지만 값 목록을 두 곳에 복제하는 책임이 생깁니다.
하이브리드 전략은 편의 기능이 아니라 릴리스 규칙입니다. 배포될 ENUM 집합과 DB 활성 코드 집합을 기계적으로 비교하고, 불일치가 있으면 애플리케이션을 시작하지 않거나 호환 모드로 제한해야 합니다.
DB에는 DELETED가 추가됐지만 한 서버의 ENUM에는 없고, 개발 분기에는 아직 DB에 없는 PAUSED가 들어갔다고 가정합니다.
문자열 변환은 런타임에서 서로 다른 방향으로 오류가 발생합니다.
코드 목록 불일치의 발견 시점
DB 값이 ENUM에 없으면 역직렬화에서 오류가 발생하고 ENUM만 앞서가면 INSERT가 FK 또는 검사에서 거절됩니다. 수동 문서 확인은 대소문자·비활성 코드·구버전 서버의 호환 구간을 놓칩니다.
DROP TEMPORARY TABLE IF EXISTS release_enum_bad;
CREATE TEMPORARY TABLE release_enum_bad (
code VARCHAR(40) NOT NULL PRIMARY KEY
);
INSERT INTO release_enum_bad (code)
VALUES ('DRAFT'), ('PUBLISHED'), ('HIDDEN'), ('PAUSED');
SELECT db.code AS db_only
FROM final_code AS db
LEFT JOIN release_enum_bad AS app ON app.code = db.code
WHERE db.group_code = 'POST_STATUS'
AND db.is_active = TRUE
AND app.code IS NULL;
SELECT app.code AS app_only
FROM release_enum_bad AS app
LEFT JOIN final_code AS db
ON db.group_code = 'POST_STATUS'
AND db.code = app.code
AND db.is_active = TRUE
WHERE db.code IS NULL;DB 전용 DELETED와 앱 전용 PAUSED가 각각 한 행 나옵니다.
어느 방향의 요청도 안전하지 않습니다.
db_only
DELETED
app_only
PAUSED
release compatible: noENUM과 코드 테이블은 역할이 다르지만 식별자 집합은 같아야 합니다. 표시 이름·정렬·활성은 DB가, 분기 로직과 타입은 앱이 소유하되 릴리스마다 교집합이 아닌 정확한 집합 일치를 확인합니다.
새 코드 배포는 DB에 먼저 비활성 또는 미사용 상태로 배포하고 모든 앱이 이해한 뒤 활성화할 수 있습니다. 코드 제거는 더 어렵기 때문에 읽기 호환 기간과 과거 행 해석을 남깁니다.
데이터베이스 값과 애플리케이션 값의 불일치
- 양방향 차집합 — DB-전용과 애플리케이션-전용을 각각 계산합니다.
- 활성 범위 — 비활성 과거 코드를 ENUM이 계속 읽어야 하는지 정합니다.
- 배포 순서 — DB 등록·앱 배포·활성화 단계를 분리합니다.
- 전이 — 코드 존재와 허용 상태 전이를 다른 규칙으로 관리합니다.
열거형과 코드 테이블
앱은 ENUM으로 분기 가능한 상태를 제한하고 DB는 안정적인 코드 ID와 표시·정렬·활성 정보를 관리합니다. 같은 식별자 집합을 공유하지만 한쪽이 다른 쪽의 책임을 대신하지 않습니다.
DRAFT → HIDDEN처럼 값이 존재한다고 모든 전이가 허용되는 것은 아닙니다.
허용 전이는 별도 관계로 표현하거나 서비스 상태 머신이 소유하고 DB 릴리스 목록과 함께 검증합니다.
타입·메타데이터·전이의 소유권 나누기
- 식별자 — DB 코드와 ENUM 이름을 바이트 단위로 같은 식별자로 고정합니다.
- 메타데이터 — 문구·정렬·운영 속성은 DB 행에서 읽습니다.
- 로직 — 전이·행동 분기는 타입 안전한 앱 코드가 담당합니다.
- 릴리스 검증 — 배포 산출물의 코드 목록을 DB 집합과 비교합니다.
릴리스 목록과 상태 전이
릴리스별 ENUM 목록을 적재하는 목록과 허용 전이 표를 만듭니다. 정상 릴리스는 DB 활성 상태 네 개와 정확히 일치하고 전이 표의 양쪽 값도 공통 코드 FK를 통과합니다.
DROP TABLE IF EXISTS final_post_status_transition;
DROP TABLE IF EXISTS final_release_enum_code;
CREATE TABLE final_release_enum_code (
release_id VARCHAR(40) NOT NULL,
group_code VARCHAR(40) NOT NULL,
code VARCHAR(40) NOT NULL,
loaded_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (release_id, group_code, code),
CONSTRAINT fk_release_enum_code FOREIGN KEY (group_code, code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT,
CONSTRAINT chk_release_enum_group
CHECK (group_code = 'POST_STATUS')
) ENGINE=InnoDB;
CREATE TABLE final_post_status_transition (
group_code VARCHAR(40) NOT NULL DEFAULT 'POST_STATUS',
from_code VARCHAR(40) NOT NULL,
to_code VARCHAR(40) NOT NULL,
transition_name VARCHAR(100) NOT NULL,
PRIMARY KEY (group_code, from_code, to_code),
CONSTRAINT chk_transition_group
CHECK (group_code = 'POST_STATUS'),
CONSTRAINT chk_transition_self CHECK (from_code <> to_code),
CONSTRAINT fk_transition_from FOREIGN KEY (group_code, from_code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT,
CONSTRAINT fk_transition_to FOREIGN KEY (group_code, to_code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT
) ENGINE=InnoDB;
INSERT INTO final_release_enum_code
(release_id, group_code, code)
VALUES
('2026.07', 'POST_STATUS', 'DRAFT'),
('2026.07', 'POST_STATUS', 'PUBLISHED'),
('2026.07', 'POST_STATUS', 'HIDDEN'),
('2026.07', 'POST_STATUS', 'DELETED');
INSERT INTO final_post_status_transition
(from_code, to_code, transition_name)
VALUES
('DRAFT', 'PUBLISHED', '게시 시작'),
('DRAFT', 'DELETED', '초안 삭제'),
('PUBLISHED', 'HIDDEN', '게시 숨김'),
('PUBLISHED', 'DELETED', '게시글 삭제');릴리스 목록과 활성 DB 집합의 양방향 차집합이 모두 0행입니다. 전이 표는 존재하는 게시글 상태끼리의 허용 간선만 저장합니다.
개선 실행 결과release_id: 2026.07
manifest codes: 4
active DB codes: 4
db-only rows: 0
app-only rows: 0
allowed transitions: 4목록은 앱 바이너리가 실제로 이해하는 값을 빌드 과정에서 생성해야 합니다. 사람이 SQL을 손으로 맞추면 다시 불일치할 수 있으므로 CI 산출물을 임시 적재 테이블에 적재해 비교하는 흐름이 필요합니다.
상태 전이 표를 운영자가 즉시 수정할 수 있게 하면 앱 로직과 다시 충돌합니다. 전이 소유권을 DB로 둘지 앱으로 둘지 정하고, 하이브리드라면 같은 릴리스 검증을 적용합니다.
릴리스 집합과 금지 간선을 함께 검증하기
- 산출물 적재 — 배포 ENUM에서 생성한 목록을
release_id로 넣습니다. - DB-전용 — 활성 DB 코드 중 목록에 없는 값을 찾습니다.
- 애플리케이션-전용 — 목록 중 DB에 없거나 비활성인 값을 찾습니다.
- 전이 — 허용·금지 간선을 게시글 상태 변경 테스트와 연결합니다.
코드 집합과 금지 전이 검증
두 차집합을 UNION ALL해 한 행이라도 나오면 배포를 중단합니다. 코드 집합이 맞아도 금지 전이는 별도 쿼리로 거절되어야 합니다.
SELECT 'DB_ONLY' AS mismatch_type, db.code
FROM final_code AS db
LEFT JOIN final_release_enum_code AS app
ON app.release_id = '2026.07'
AND app.group_code = db.group_code
AND app.code = db.code
WHERE db.group_code = 'POST_STATUS'
AND db.is_active = TRUE
AND app.code IS NULL
UNION ALL
SELECT 'APP_ONLY' AS mismatch_type, app.code
FROM final_release_enum_code AS app
LEFT JOIN final_code AS db
ON db.group_code = app.group_code
AND db.code = app.code
AND db.is_active = TRUE
WHERE app.release_id = '2026.07'
AND app.group_code = 'POST_STATUS'
AND db.code IS NULL;
SELECT EXISTS (
SELECT 1
FROM final_post_status_transition
WHERE from_code = 'DRAFT'
AND to_code = 'HIDDEN'
) AS draft_hide_allowed;
SELECT EXISTS (
SELECT 1
FROM final_post_status_transition
WHERE from_code = 'PUBLISHED'
AND to_code = 'HIDDEN'
) AS published_hide_allowed;불일치 쿼리는 0행입니다.
DRAFT → HIDDEN는 0, PUBLISHED → HIDDEN는 1이므로 존재 가능한 값과 허용 가능한 변화가 구분됩니다.
활성화와 이름 변경 순서를 바꾸는 호환 실험
- DB 선행 — PAUSED를 비활성 코드로 먼저 등록하고 현재 릴리스 불일치를 확인합니다.
- 앱 배포 — 다음 릴리스 목록에 PAUSED를 넣되 활성 집합 비교 기준을 조정합니다.
- 순차 — 구버전 목록이 새 쓰기 주체의 값을 읽을 수 있는지 호환표를 만듭니다.
- 이름 변경 — 과거 REMOVED를 표준 DELETED로 모으는 확장-규칙 단계를 적습니다.
순차 배포에서 새 코드가 먼저 보이는 문제
순차 배포 중 구버전 서버가 새 코드를 읽을 수 있습니다. 새 DB 코드를 먼저 비활성 등록하고 모든 버전이 역직렬화할 수 있게 배포한 뒤 쓰기 주체를 활성화하는 확장-규칙 순서를 사용합니다.
ENUM 이름을 바꾸는 것은 DB PK 변경과 같습니다. 새 이름을 추가하고 앱이 두 이름을 읽되 새 이름만 쓰는 기간을 거친 뒤 과거 데이터를 백필하고 이전 코드를 중단합니다.
모바일 클라이언트·작성자 전용·복합 전이의 예외
- 비활성 코드는 신규 선택에서 빠져도 과거 행을 읽는 ENUM에는 남아야 할 수 있습니다.
- 모바일 클라이언트처럼 강제 업그레이드가 어려운 소비자는 더 긴 읽기 호환 기간이 필요합니다.
- 외부 API에 공개된 코드 이름 변경은 내부 DB 변경보다 버전이 있는 API 규칙이 우선입니다.
- 전이 조건이 사용자 권한·시간·잔액에 의존하면 단순 간선 표만으로 충분하지 않습니다.
DB·ENUM·하이브리드 규칙 비교
| 선택 | 얻는 효과 | 감수할 비용 | 적합한 조건 |
|---|---|---|---|
| DB 코드만 | 운영 변경이 빠름 | 앱 타입·분기 안전성 약함 | 단순 선택 목록일 때 |
| 앱 ENUM만 | 컴파일 검증·간단 | 표시·속성 변경에 재배포 | 로직 중심 고정 상태일 때 |
| 하이브리드 | 타입과 운영 메타데이터 결합 | 동기화 검증 단계 필요 | 상태 로직과 표시가 모두 중요할 때 |
| 전이 표 DB 소유 | 운영 규칙 가시화 | 앱 상태 머신 중복 | 전이를 중앙 설정할 명확한 이유가 있을 때 |
알 수 없음 코드 오류와 구버전 비율 관찰하기
배포마다 릴리스별 DB-전용/애플리케이션-전용 수, 구버전 서버 비율, 알 수 없는 코드 역직렬화 오류를 기록합니다.
코드 추가·중단·별칭 기간과 쓰기 주체 활성 시각을 변경 기록에 남깁니다.
다음 장에서는 부모 ID 한 칸만 저장한 인접 리스트에서 깊이가 가변적인 회원 게시판 주제 분류를 조회하고 이동하는 방법을 다룹니다.
DB 코드 연동 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 로직 | 코드별 분기와 허용 행동이 애플리케이션에 존재하는가? |
| 운영 | 이름·정렬·속성을 재배포 없이 바꿔야 하는가? |
| 검증 | 릴리스 산출물과 DB 집합을 자동 비교하는가? |
| 호환 | 순차 배포 중 이전/신규 읽기 주체·쓰기 주체 조합을 설명하는가? |
| 전이 | 값의 존재와 상태 변화 규칙의 소유자를 구분하는가? |
하이브리드는 두 저장소를 쓰는 만큼 검증 장치가 있어야 합니다. 릴리스 검증이 없다면 ENUM과 코드 테이블의 장점보다 불일치 위험이 더 큽니다.
연습 문제
과거 시스템의 REMOVED를 표준 상태 DELETED로 모으는 호환 별칭을 설계하세요.
이전 행을 계속 읽을 수 있고 신규 쓰기 주체는 DELETED만 쓰며 최종적으로 REMOVED 참조 수를 0으로 만들 수 있어야 합니다.
해설과 예시 답안
이전 코드와 신규 코드를 모두 상세 코드에 두고 별칭 관계를 명시합니다. 읽기 계층은 이전 코드를 정규형 신규 코드로 변환하고 백필 숨김 뒤 이전 코드를 비활성화합니다.
DROP TABLE IF EXISTS final_code_alias;
CREATE TABLE final_code_alias (
group_code VARCHAR(40) NOT NULL,
alias_code VARCHAR(40) NOT NULL,
canonical_code VARCHAR(40) NOT NULL,
valid_until DATETIME(6) NOT NULL,
PRIMARY KEY (group_code, alias_code),
CONSTRAINT chk_code_alias_distinct
CHECK (alias_code <> canonical_code),
CONSTRAINT fk_code_alias_old FOREIGN KEY (group_code, alias_code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT,
CONSTRAINT fk_code_alias_new FOREIGN KEY (group_code, canonical_code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT
) ENGINE=InnoDB;
INSERT INTO final_code
(group_code, code, display_name_ko, sort_order)
VALUES ('POST_STATUS', 'REMOVED', '과거 삭제', 50);
INSERT INTO final_code_alias
(group_code, alias_code, canonical_code, valid_until)
VALUES
('POST_STATUS', 'REMOVED', 'DELETED', '2026-10-01 00:00:00');
INSERT INTO final_post_code_demo
(post_status, visibility_code, view_count)
VALUES ('REMOVED', 'PRIVATE', 0);
SELECT d.post_id,
COALESCE(a.canonical_code, d.post_status) AS canonical_status
FROM final_post_code_demo AS d
LEFT JOIN final_code_alias AS a
ON a.group_code = d.status_group_code
AND a.alias_code = d.post_status;과거 REMOVED 행은 DELETED로 읽히고 신규 쓰기 주체가 DELETED만 쓰는지 확인합니다. 백필 후 REMOVED 참조 수가 0일 때만 비활성화합니다.
핵심 정리
- ENUM과 DB 코드는 역할이 달라도 식별자 집합은 동기화되어야 합니다.
- 양방향 차집합이 릴리스 불일치를 확인합니다.
- 코드 존재와 허용 상태 전이는 별도 규칙입니다.
- 추가·이름 변경은 순차 배포의 읽기·쓰기 호환 순서를 따릅니다.
다음 장에서는 가변 깊이 주제 분류를 인접 리스트와 재귀 CTE로 모델링합니다.