코드별 확장 속성
attr1·attr2의 의미 충돌을 재현하고 게시글 공개 범위 코드에만 필요한 정책을 명시적인 1:1 확장 테이블로 모델링합니다.
공통 코드가 자리를 잡으면 곧 attr1, attr2를 붙여 모든 그룹의 추가 정보를 담고 싶은 유혹이 생깁니다.
컬럼은 적게 늘지만 같은 칸이 그룹마다 할인율, 캐시 초, 색상처럼 다른 뜻을 갖습니다.
유연성은 이름을 지우는 데서 생기지 않습니다.
어떤 코드에 어떤 정책이 붙는지 관계로 표현하고 타입·필수값·단위를 DB가 확인할 수 있어야 운영자가 값을 안전하게 바꿀 수 있습니다.
게시글 공개 범위 중 PUBLIC은 검색 결과에 노출할 수 있고, MEMBERS_ONLY와 PRIVATE는 로그인 또는 작성자 확인이 필요하다고 가정합니다.
범용 슬롯 한 칸으로 세 정책을 섞으면 조회자가 숫자의 단위를 추측해야 합니다.
범용 속성 슬롯의 모호성
컬럼 이름에 의미가 없으므로 SQL은 그룹별 CASE를 반복합니다.
문자열에 숫자와 불리언을 함께 넣으면 잘못된 단위와 타입도 정상 저장되고, 정책 하나를 필수로 만들 검사를 작성하기 어렵습니다.
DROP TABLE IF EXISTS code_attribute_bad;
CREATE TABLE code_attribute_bad (
group_code VARCHAR(40) NOT NULL,
code VARCHAR(40) NOT NULL,
attr1 VARCHAR(100) NULL,
attr2 VARCHAR(100) NULL,
PRIMARY KEY (group_code, code)
);
INSERT INTO code_attribute_bad
(group_code, code, attr1, attr2)
VALUES
('POST_VISIBILITY', 'PRIVATE', 'Y', NULL),
('POST_VISIBILITY', 'MEMBERS_ONLY', '60', '200'),
('MEMBER_GRADE', 'VIP', '5', 'gold');
SELECT group_code,
code,
CAST(attr1 AS UNSIGNED) AS cache_ttl_seconds
FROM code_attribute_bad;PRIVATE의 Y는 숫자 변환에서 0이 되고 VIP 할인율 5는 캐시 초처럼 보입니다.
SQL 오류 없이 의미가 바뀝니다.
오류 실행 결과group_code | code | cache_ttl_seconds
POST_VISIBILITY | PRIVATE | 0
POST_VISIBILITY | MEMBERS_ONLY | 60
MEMBER_GRADE | VIP | 5
semantic validation errors raised: 0범용 슬롯은 컬럼 수를 줄였을 뿐 스키마를 문서 밖으로 밀어냈습니다.
그룹 이름을 알아야 속성 위치와 단위를 해석할 수 있어 임시 쿼리와 데이터 품질 검사가 모두 취약합니다.
모든 코드가 같은 속성을 공유하지 않는다면 상세 코드 행을 넓히지 말고 해당 그룹을 위한 1:1 확장을 둡니다.
명시적 이름과 타입으로 제약을 걸고 없는 정책은 행 없음으로 표현합니다.
attr1의 숫자가 세 단위로 갈라지는 순간
- 단위 — 각 속성 값이 초·글자 수·불리언 중 무엇인지 표기합니다.
- 범위 — 속성이 모든 그룹에 공통인지 한 그룹에만 필요한지 나눕니다.
- 필수 — 코드별로 반드시 필요한 값과 선택값을 구분합니다.
- 조회 — CASE 없이 컬럼 이름만 보고 조건을 작성할 수 있는지 확인합니다.
공통 코드와 확장 정책
이름·정렬·활성처럼 모든 코드가 공유하는 메타데이터는 final_code에 남깁니다.
공개 범위의 로그인·캐시·검색 미리보기 정책처럼 특정 그룹만 갖는 속성은 그 그룹 전용 테이블에 둡니다.
확장 테이블의 복합 PK를 그대로 공통 코드 FK로 연결하면 존재하지 않는 코드 정책을 만들 수 없습니다.
그룹 검사를 더하면 다른 그룹 코드가 잘못 확장되는 것도 차단합니다.
공통 메타데이터와 공개 범위 정책의 경계를 긋기
- 공통부 — 모든 그룹이 사용하는 이름·순서·활성만 기본에 둡니다.
- 전용부 — 로그인 필요 여부·캐시 TTL·검색 미리보기 길이를 이름 있는 타입으로 선언합니다.
- 수명 — 코드 삭제·중단과 정책 행 삭제가 함께 움직일지 정합니다.
- 기본값 — 정책 행 없음과 값 0이 같은지 다른지 공개 범위 규칙에 적습니다.
복합 키 기반 정책 확장
ch13-1의 final_code를 부모로 사용합니다.
세 공개 범위 코드마다 정책 행을 하나씩 두고 불리언·초·글자 수 단위를 컬럼 이름과 검사로 고정합니다.
DROP TABLE IF EXISTS final_post_visibility_policy;
CREATE TABLE final_post_visibility_policy (
group_code VARCHAR(40) NOT NULL DEFAULT 'POST_VISIBILITY',
visibility_code VARCHAR(40) NOT NULL,
requires_login BOOLEAN NOT NULL,
cache_ttl_seconds SMALLINT UNSIGNED NOT NULL,
search_preview_chars SMALLINT UNSIGNED NULL,
PRIMARY KEY (group_code, visibility_code),
CONSTRAINT chk_visibility_policy_group
CHECK (group_code = 'POST_VISIBILITY'),
CONSTRAINT chk_visibility_policy_login
CHECK (requires_login IN (0, 1)),
CONSTRAINT chk_visibility_policy_cache
CHECK (cache_ttl_seconds BETWEEN 0 AND 3600),
CONSTRAINT chk_visibility_policy_preview CHECK (
(visibility_code = 'PUBLIC' AND search_preview_chars BETWEEN 80 AND 1000)
OR
(visibility_code <> 'PUBLIC' AND search_preview_chars IS NULL)
),
CONSTRAINT fk_visibility_policy_code FOREIGN KEY
(group_code, visibility_code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT
) ENGINE=InnoDB;
INSERT INTO final_post_visibility_policy
(visibility_code, requires_login, cache_ttl_seconds, search_preview_chars)
VALUES
('PUBLIC', FALSE, 300, 200),
('MEMBERS_ONLY', TRUE, 60, NULL),
('PRIVATE', TRUE, 0, NULL);각 숫자의 단위와 허용 범위가 스키마에 드러납니다.
전체 공개가 아닌 코드에 검색 미리보기 길이를 넣거나 다른 그룹 코드를 확장하면 저장이 거절됩니다.
개선 실행 결과visibility_code | requires_login | cache_ttl_seconds | search_preview_chars
PUBLIC | 0 | 300 | 200
MEMBERS_ONLY | 1 | 60 | NULL
PRIVATE | 1 | 0 | NULL정책이 코드와 항상 한 행씩 존재해야 한다면 신규 코드 등록 트랜잭션에서 두 INSERT를 함께 처리하고 누락 일치 검사를 둡니다.
FK만으로는 부모마다 자식이 반드시 존재하는 전체 참여를 강제하지 못합니다.
그룹별 확장이 너무 많아지고 속성이 운영 중 동적으로 바뀐다면 EAV나 JSON을 검토할 수 있습니다.
그러나 고정된 핵심 정책까지 동적 모델로 옮기면 타입과 검색 비용을 잃습니다.
범위 위반과 참여 누락을 별도로 확인하기
- 정상 정책 — 세 공개 범위의 로그인·캐시 TTL·미리보기 값을 입력합니다.
- 단위 위반 —
cache_ttl_seconds4000을 넣어 검사를 확인합니다. - 그룹 위반 —
POST_STATUS코드를 공개 범위 정책에 연결해 거절되는지 봅니다. - 참여 일치 검사 — 활성 공개 범위 코드 중 정책 행이 없는 값을 찾습니다.
타입과 필수 정책 검증
행 내부 범위는 검사가 막고 부모·자식 참여 누락은 안티 조인으로 찾습니다.
둘을 구분해야 제약이 보장하지 않는 규칙을 운영 일치 검사가 맡을 수 있습니다.
INSERT INTO final_post_visibility_policy
(visibility_code, requires_login, cache_ttl_seconds, search_preview_chars)
VALUES ('MEMBERS_ONLY', TRUE, 4000, NULL);
-- ERROR 3819: chk_visibility_policy_cache 또는 PK 1062
UPDATE final_post_visibility_policy
SET search_preview_chars = 200
WHERE visibility_code = 'PRIVATE';
-- ERROR 3819: chk_visibility_policy_preview
SELECT c.code AS missing_policy_code
FROM final_code AS c
LEFT JOIN final_post_visibility_policy AS p
ON p.group_code = c.group_code
AND p.visibility_code = c.code
WHERE c.group_code = 'POST_VISIBILITY'
AND c.is_active = TRUE
AND p.visibility_code IS NULL;
SELECT c.code,
c.display_name_ko,
p.requires_login,
p.cache_ttl_seconds,
p.search_preview_chars
FROM final_code AS c
JOIN final_post_visibility_policy AS p
ON p.group_code = c.group_code
AND p.visibility_code = c.code
WHERE c.group_code = 'POST_VISIBILITY'
ORDER BY c.sort_order;위반 UPDATE는 오류가 발생하고 안티 조인은 0행이어야 합니다.
조회자는 그룹별 CASE 없이 컬럼 이름과 타입으로 세 정책을 해석합니다.
기본·확장·EAV·JSON 배치 비교
| 선택 | 얻는 효과 | 감수할 비용 | 적합한 조건 |
|---|---|---|---|
| 기본 컬럼 추가 | 조회와 제약이 단순 | 대부분 그룹에 NULL | 모든 코드가 같은 속성을 가질 때 |
| 그룹 확장 | 명시적 타입·낮은 NULL | 그룹별 테이블 증가 | 정책 집합이 고정될 때 |
| EAV 속성 | 운영 중 정의 추가 | 타입·조회·검증 복잡 | 정말 동적인 메타데이터일 때 |
| JSON 정책 | 객체 단위 입출력 | 경로 제약·인덱스 비용 | 드문 비핵심 속성일 때 |
정책 효력 시점과 RESTRICT가 남기는 책임
정책 변경은 표시 이름 변경보다 위험합니다.
캐시 TTL을 300초에서 60초로 줄이면 기존 캐시를 즉시 만료할지 신규 응답부터 적용할지 효력 시간이 필요할 수 있습니다.
확장 테이블을 ON DELETE CASCADE로 두면 코드 정리 실수가 정책까지 지웁니다.
과거 행 해석을 중시하므로 여기서는 RESTRICT와 명시적 중단 절차를 선택했습니다.
0·NULL·테넌트 재정의에서 달라지는 의미
- 캐시 TTL 0은 캐시하지 않는다는 뜻이며 영구 캐시와 구분합니다.
search_preview_chars의 NULL은 제한 없음이 아니라 검색 미리보기를 만들지 않는다는 뜻입니다.- 테넌트별 정책은 전역 코드 확장과 별도 재정의 테이블로 나눌 수 있습니다.
- 정책 효력 기간이 필요해지면 현재 행 UPDATE 대신 버전 테이블을 사용합니다.
캐시 TTL 변경의 영향 행을 관찰하기
정책 변경에는 이전 값·변경자·효력 시점·영향 받은 캐시 수를 감사 기록으로 남깁니다.
활성 코드 대비 정책 누락 수, 범위 제약 오류, 캐시 TTL 만료 대기 건수를 관찰합니다.
단위와 필수 참여를 흔드는 정책 실험
- 범위 — 캐시 TTL을 3601초로 바꾸어 오류를 확인합니다.
- 타입 —
requires_login에 2를 넣어 불리언 검사를 봅니다. - 참여 — 새 활성 공개 범위 코드를 정책 없이 추가하고 안티 조인을 실행합니다.
- 수명 — PRIVATE 코드를 비활성화했을 때 기존 정책을 유지할지 기록합니다.
다음 문서는 코드 이름과 정책이 바뀌었을 때 여러 서버의 로컬 캐시가 언제 새 버전을 읽었는지 추적하는 규칙을 만듭니다.
코드 속성 배치 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 공통성 | 모든 코드 그룹에 같은 뜻으로 존재하는가? |
| 타입 | DB 타입과 검사로 잘못된 값을 막아야 하는가? |
| 변경 | 운영 중 속성 정의 자체가 자주 추가되는가? |
| 참여 | 모든 코드가 반드시 정책 한 행을 가져야 하는가? |
| 이력 | 정책의 과거 효력 구간을 조회해야 하는가? |
범용성은 속성 번호를 늘리는 것이 아니라 변하는 축을 정확히 고르는 일입니다.
고정된 정책은 이름 있는 컬럼과 전용 확장이 가장 읽기 쉽습니다.
연습 문제
코드 표시 이름을 ko-KR, en-US 두 로캘로 제공하는 표시명 확장을 설계하세요.
같은 코드·로캘 중복과 존재하지 않는 코드의 번역을 막아야 합니다.
해설과 예시 답안
번역은 모든 그룹에 공통으로 적용될 수 있으므로 코드별 다국어 표시명 테이블을 둡니다.
복합 코드 키 뒤에 로캘을 추가한 PK가 한 언어당 한 행을 보장합니다.
CREATE TABLE final_code_label (
group_code VARCHAR(40) NOT NULL,
code VARCHAR(40) NOT NULL,
locale VARCHAR(20) NOT NULL,
display_name VARCHAR(100) NOT NULL,
PRIMARY KEY (group_code, code, locale),
CONSTRAINT fk_code_label_code FOREIGN KEY (group_code, code)
REFERENCES final_code (group_code, code) ON DELETE RESTRICT,
CONSTRAINT chk_code_label_locale
CHECK (locale REGEXP '^[a-z]{2}(-[A-Z]{2})?#x27;)
) ENGINE=InnoDB;
INSERT INTO final_code_label
(group_code, code, locale, display_name)
VALUES
('POST_STATUS', 'PUBLISHED', 'ko-KR', '게시됨'),
('POST_STATUS', 'PUBLISHED', 'en-US', 'PUBLISHED');같은 (group, code, locale) 재입력은 1062, 없는 코드 번역은 1452, 잘못된 로캘은 3819여야 합니다.
핵심 정리
- 속성 번호는 의미와 단위를 스키마 밖으로 밀어냅니다.
- 공통 메타데이터와 그룹별 정책을 분리합니다.
- 전용 확장은 명시적 타입·검사·복합 FK를 제공합니다.
- 부모마다 정책이 필요한 규칙은 안티 조인 일치 검사까지 둡니다.
다음 문서에서는 코드 변경을 버전과 아웃박스로 발행해 캐시의 오래된 상태를 측정합니다.