append-only 정형 로그. 플래그별 이력 조회(관계형 조회). 기존 mcp_audit_logs 선례와 동일 패턴
MongoDB 미채택 (1줄 근거): 두 단위 모두 정형 스키마 + UNIQUE 제약 + 낙관락 + 관계형 조회(플래그별 감사)로, private-mongodb-convention의 문서형/스키마리스·대량 로그·중첩 트리 채택 근거에 해당하지 않는다. 전부 MySQL. (strategy_config는 JSON “문자열”을 TEXT에 담을 뿐, 문서 DB가 아니다 — 전략 내부는 쿼리 대상이 아니라 애플리케이션 AttributeConverter로만 역직렬화한다.)
Redis(캐시·pub/sub)는 평가 성능·전파용 보조 계층으로 TDD 방안 A에 이미 확정 — 저장소가 아니라 캐시다. SSOT는 MySQL이며 Redis는 파생본이다.
테이블 정의
1. feature_flags — 플래그 상태·평가 전략 (V43, SSOT)
컬럼
타입
NULL
기본값
COMMENT / 근거
id
BIGINT AUTO_INCREMENT
NOT NULL
—
PK
flag_key
VARCHAR(100)
NOT NULL
—
평가 키 (예: demo.feature.hello). UNIQUE — 평가 조회(findByKey)·중복 검증(existsByKey)의 대상. 길이 100은 mcp_audit_logs.tool_name 선례 준용
상태: ACTIVE | ARCHIVED (ENUM 금지 → VARCHAR). 생성 시 ACTIVE — DEFAULT로 명시
description
VARCHAR(500)
NULL
—
플래그 설명 (운영자 입력). 선택 입력이라 NULL 허용
strategy_config
TEXT
NOT NULL
—
평가 전략 JSON 문자열 (sealed EvaluationStrategy ↔ AttributeConverter). JSON 컬럼 금지 → TEXT (private-db-schema-convention). 전략 내부는 쿼리 대상 아님(방안 J)
version
BIGINT
NOT NULL
0
낙관락(@Version) — 관리 API 동시 수정 lost-update 방지. BIGINT(Long) 확정 (senior-pm 정합): TDD ERD의 int 표기에서 조정, 레포 선례(payments.version·mcp_tokens.version BIGINT) 및 BE-01 @Version Long과 정합
created_at
DATETIME(6)
NOT NULL
—
생성 시각 (UTC)
created_by
BIGINT
NULL
—
생성자 user_id (ADMIN, SecurityAuditorAware). 물리 FK 없음
updated_at
DATETIME(6)
NOT NULL
—
마지막 수정 시각
updated_by
BIGINT
NULL
—
마지막 수정자 user_id. 물리 FK 없음
soft-delete 미도입: 라이프사이클은 status(ACTIVE↔ARCHIVED)로 완결한다. archive는 삭제가 아니라 평가 제외 상태 전이(User Scenario 9). deleted_at 없음 — 단순함 우선, partner 선례와 동일.
전제 (중요): 평가 트래픽(20000 TPS)은 로컬 인메모리 스냅샷 → Redis 캐시에서 처리되며 MySQL에 도달하지 않는다(TDD 방안 A·NFR). MySQL 읽기는 캐시 미스 + 기동 시 부트스트랩(findAllActive) 에만 발생한다. 따라서 인덱스 설계는 고TPS 평가가 아니라 관리 쓰기·저빈도 조회 기준이다.
feature_flags
#
쿼리 패턴
WHERE / ORDER
인덱스
근거
Q1
평가·중복검증 단건 조회 (findByKey, existsByKey)
WHERE flag_key = ?
UNIQUE KEY uq_feature_flags_flag_key (flag_key)
가장 빈번한 조회 경로(캐시 미스 시 look-aside 채움). UNIQUE가 조회 인덱스 + 중복 방지 비즈니스 규칙을 동시 충족. flag_key는 카디널리티 = 행 수(고유)
Q2
활성 목록 부트스트랩 (findAllActive)
WHERE status = 'ACTIVE'
(인덱스 없음 — 아래 판단 참조)
기동 시 1회. 테이블 저볼륨
Q3
관리 화면 동적 필터 (findAll(status, type))
WHERE status = ? AND flag_type = ? (동적)
(인덱스 없음 — 아래 판단 참조)
운영자 화면 전용, 극저빈도
status 인덱스 — 미채택 확정 (senior-pm 정합)
TDD §ERD 요구사항은 “status 인덱스(findAllActive·목록)“를 명시했으나, 시니어 DBA 판단으로 생성하지 않는 것으로 확정한다 — 근거:
카디널리티 2 (ACTIVE/ARCHIVED) — 단일 컬럼 저카디널리티 인덱스는 옵티마이저가 무시하기 쉽다.
테이블 저볼륨 — 플래그 총 수는 수십~수백 행 예상(용량 추정 참조). WHERE status='ACTIVE' 풀스캔이 <1ms.
조회 빈도 극저 — findAllActive는 인스턴스 기동 시 1회, 동적 필터는 관리 화면 전용.
private-db-schema-convention “근거 없는 인덱스는 만들지 않는다 (쓰기 비용)” 원칙에 정합.
→ 확정: status 인덱스는 생성하지 않는다. 플래그 수가 1,000행을 초과하거나 findAllActive가 핫패스가 되면 그때 ALGORITHM=INPLACE, LOCK=NONE으로 온라인 추가한다(후속 판단). 1차 인덱스는 Q1의 UNIQUE만.
feature_flag_audit_logs
#
쿼리 패턴
WHERE / ORDER
인덱스
근거 (ESR)
Q4
플래그별 이력 최신순 페이징 (findByFlagKey)
WHERE flag_key = ? ORDER BY occurred_at DESC LIMIT ? OFFSET ?
INDEX idx_feature_flag_audit_logs_flag_key_occurred_at (flag_key, occurred_at)
복합 컬럼 순서 근거: flag_key는 Equality(선두, 특정 플래그로 필터 — 고선택도), occurred_at은 Sort(정렬·범위, 두 번째). 커버링 정렬로 filesort 제거. MySQL은 인덱스 역순 스캔으로 DESC 처리
보존 삭제 스캔용 occurred_at 단독 인덱스 — 미채택: 보존 정책(1년) 배치 DELETE WHERE occurred_at < ?는 복합 인덱스 선두가 flag_key라 활용 못 한다. 그러나 감사 테이블이 저볼륨(용량 추정: ~2MB/년)이라 배치 풀스캔이 무시 가능하고, 배치는 극저빈도(일/주 1회)다 → 단독 인덱스의 쓰기 비용이 이득을 초과. 미도입.
actor·change_type 인덱스 — 미채택: PRD/TDD에 “행위자별·종류별 조회” 쿼리 패턴이 없다. 근거 쿼리 없는 인덱스는 만들지 않는다.
용량 추정 · 보존 정책
추정 기준: 평가 트래픽은 DB 미도달(캐시/스냅샷). 관리 쓰기·감사 적재만이 DB 부하다.
feature_flags
항목
추정
산출 근거
행 수 (1년 후)
50200행
신규 기능마다 1플래그. 개인 프로젝트 규모 + 아카이브 누적. RELEASE는 정리 후보 알림(FR-14)으로 감축
행 크기
0.51KB
strategy_config TEXT가 소형 JSON(전략 파라미터 몇 개). variant 4개여도 <1KB
1년 후 테이블 크기
< 1MB
무시 가능
증가율
신규 기능 도입 속도에 비례 (연 수십 행)
—
보존: 영구 보존(무한 성장 아님). ARCHIVED도 이력·재활성 가능성 때문에 물리 삭제하지 않는다. 저볼륨이라 아카이빙 불필요.
feature_flag_audit_logs
항목
추정
산출 근거
적재율
관리 변경 1건당 1행 (create/update/archive/activate)
평가는 감사 대상 아님
연간 행 수
1,5002,000행
활성 롤아웃기 피크 ~20건/일, 평균 ~5건/일 가정 → 연 ~1,800건
행 크기
~1KB
before/after_snapshot TEXT 2개(각 200500B) + 메타
1년 후 크기
1.52MB
무시 가능
보존 정책 (NFR: 최소 1년): 1차는 삭제 없이 유지하기를 권고 — 연 2MB는 저장 비용이 사실상 0이고, 1년 초과분을 지워야 할 볼륨 압박이 없다. 보존 삭제가 필요해지면 @Scheduled 배치로 DELETE WHERE occurred_at < NOW() - INTERVAL 1 YEAR(저볼륨 풀스캔, 온라인 무해).
파티셔닝·아카이빙 — 미채택 (오버엔지니어링): 연 2MB 규모에 시간 파티셔닝은 과설계. private-senior-dba “개인 프로젝트 규모에 과한 설계 미채택” 원칙 정합. 볼륨이 수천만 행대로 커지면 그때 월 파티셔닝 재검토(현재 지평 밖).
Release Scenario — expand-contract 무중단 마이그레이션
신규 도메인 순수 가산. 기존 12개 도메인·스키마 무변경. 스키마가 코드보다 먼저 배포돼도 안전(신규 테이블은 아무도 참조 안 함).
CREATE TABLE = 신규 객체 생성. 기존 테이블 무락(온라인 안전). 인덱스는 CREATE TABLE 인라인 정의라 별도 ALTER ... ADD INDEX 불필요
참조 코드 미배포 상태 → 역방향 DROP TABLE feature_flag_audit_logs; DROP TABLE feature_flags; 안전 (데이터·의존 없음)
2. 코드 배포
featureflag/featuredemo 코드 + ErrorStatus.SERVICE_UNAVAILABLE 추가. 플래그 미생성 상태 → 데모 엔드포인트는 기본값 OFF(다크)
스키마 성공 후
— (DDL 없음)
코드 직전 태그 재배포. 스키마 가산이라 테이블 잔존 무해
3. 데이터 (플래그 생성)
관리 API로 demo.feature.hello 생성 → INSERT 1행 + 감사 1행
코드 배포 후
row-level(단일 INSERT)
플래그 archive/OFF (킬스위치) — DDL 롤백 불필요
배포 순서 결론: 스키마 먼저 → 코드 → 데이터. 신규 테이블은 하위 호환 가산이라 스키마 선배포가 가장 안전하고 API 버저닝 불필요.
NOT NULL 3단계 분리 불필요: 신규 테이블의 NOT NULL 컬럼은 기존 데이터가 없어 백필 대상이 없다. CREATE TABLE에서 바로 NOT NULL 정의(private-db-schema-convention의 “NOT NULL 추가 3단계”는 기존 테이블 ALTER에만 적용).
인덱스 락: 모든 인덱스(UNIQUE uq_feature_flags_flag_key, 복합 idx_..._flag_key_occurred_at)를 CREATE TABLE 시점에 인라인 정의 → 온라인 DDL 이슈 없음. 사후 추가(예: status 인덱스 채택 시)만 ALGORITHM=INPLACE, LOCK=NONE 명시.
롤백 총평: 최악의 경우에도 (a) 게이팅 플래그 archive 한 번으로 기능 즉시 비활성(기존 도메인 무영향), (b) 시스템 롤백은 코드 직전 태그 재배포, (c) 스키마 롤백은 참조 미배포 상태에서 DROP TABLE 2개.
마이그레이션 번호 이슈
항목
값
현재 최신 (실측)
V37
형제 과제 문서상 점유
② B2B 파트너 = V38/V39/V40 · ③ limited_drops = V41 · ⑥ alerts = V42
본 과제 권고 번호
V43 (V43__create_feature_flags.sql, 2 테이블 1 파일)
충돌 가능성 (필독): V38V42는 세 형제 과제가 같은 db/migration/ 네임스페이스를 문서상으로만 점유한 잠정 배정이다(아직 dev 미머지). 공통 방침은 “먼저 dev에 머지되는 쪽이 V38부터 순차 점유하고, 나중 쪽은 origin/dev 기준 최신 번호로 재배정”이다. 따라서 V43은 잠정이며, private-mysql-implementer가 구현 시점에 origin/dev 기준 워크트리에서 최신 버전을 실측해 확정해야 한다(형제 과제 머지 지연 시 V38V42 중 빈 번호로 앞당겨질 수도, 추가 머지 시 V43+ 로 밀릴 수도 있음). MEMORY “구현 에이전트 worktree 구버전 분기” 이슈와 직결 — origin/dev 기준 워크트리 필수.
컨벤션 위반 0건: FK/ENUM/JSON/BOOLEAN 금지, DATETIME(6), COMMENT, PK id, 참조 {entity}_id 모두 충족.
TDD 조정 2건 확정 (senior-pm 정합 완료): ① version 타입 INT→BIGINT(Long) 확정(레포 선례·BE-01 정합), ② status 인덱스 미채택 확정(저카디널리티·저볼륨·근거 없는 인덱스 회피). 둘 다 근거와 함께 못박음 — 더 이상 열린 판단 아님.
Document History
날짜
변경 내용
2026-07-03
최초 작성 — TDD §ERD 요구사항 소비, 기존 마이그레이션 컨벤션 실측(V37 최신·BIGINT version·순차정수), 테이블 2종 정의, 쿼리→인덱스 매핑(status 인덱스 미채택 권고·복합 인덱스 ESR 근거), 용량 추정(관리 쓰기 기준·평가는 DB 미도달), expand-contract 배포·V43 번호 잠정 배정