피처 플래그 DB 설계 (design-db)

Background

  • 근거 PRD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/피처 플래그/PRD.md
  • 근거 TDD: /Users/biuea/Desktop/dpdpdndn/프로젝트/스포츠앱/피처 플래그/TDD.md (§ERD 하단 “senior-dba/mysql-implementer 요구사항”)
  • 대상 레포: /Users/biuea/sports-application (backend/src/main/resources/db/migration/)
  • 이 문서는 설계 산출물이다. DDL 전문 작성·실행·검증은 private-mysql-implementer가 담당한다. 이 문서로 마이그레이션을 작성할 수 있는 수준의 컬럼·타입·인덱스·근거를 확정한다.
  • 범위: 신규 테이블 2개 (feature_flags, feature_flag_audit_logs). 기존 테이블·데이터 무변경.

실측한 기존 마이그레이션 컨벤션 (SSOT)

항목실측 결과근거
파일 명명순차 정수 V{정수}__{snake_case}.sql — private-db-schema-convention의 타임스탬프 형식이 아님레포 db/migration/ 전수 (V8~V37)
최신 버전V37 (V37__fix_cart_items_active_unique.sql)실측 (find db/migration -name 'V*.sql' | sort -V)
COMMENT전 컬럼·테이블 COMMENT 필수V20__create_mcp_tokens.sql, V21__create_mcp_audit_logs.sql
인덱스 명명INDEX idx_{table}_{cols} / UNIQUE KEY uq_{table}_{cols} + 인덱스 COMMENTV21:22-25, V35
시간 컬럼DATETIME(6) (마이크로초)전수
BOOLEANTINYINT(1) (ENUM·BOOLEAN 금지)V20
status/typeVARCHAR (ENUM 금지)V33:5,9 (type VARCHAR(32), status VARCHAR(20))
FK물리 FK 없음, 일반 컬럼(user_id, created_by)전수
낙관락version BIGINT NOT NULL DEFAULT 0V30__add_payments_version.sql, V20:mcp_tokens
감사 로그append-only, created_*/이벤트 시각만 (updated/deleted 없음)V21__create_mcp_audit_logs.sql:1-5

신규 2개 테이블은 전부 가산(additive)CREATE TABLE은 신규 객체 생성이므로 기존 테이블에 락을 유발하지 않는다(온라인 안전).

저장소 선택

데이터 단위저장소채택 사유
feature_flags (플래그 상태·전략)MySQLSSOT. 관계형·트랜잭션·낙관락(@Version)·UNIQUE 제약(flag_key)·상태 전이가 본질. 저볼륨
feature_flag_audit_logs (변경 감사)MySQLappend-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 / 근거
idBIGINT AUTO_INCREMENTNOT NULLPK
flag_keyVARCHAR(100)NOT NULL평가 키 (예: demo.feature.hello). UNIQUE — 평가 조회(findByKey)·중복 검증(existsByKey)의 대상. 길이 100은 mcp_audit_logs.tool_name 선례 준용
flag_typeVARCHAR(32)NOT NULL종류: RELEASE | OPERATIONAL | EXPERIMENT | ENTITLEMENT (ENUM 금지 → VARCHAR). 값 검증은 애플리케이션(FeatureFlagType enum)
statusVARCHAR(20)NOT NULL'ACTIVE'상태: ACTIVE | ARCHIVED (ENUM 금지 → VARCHAR). 생성 시 ACTIVE — DEFAULT로 명시
descriptionVARCHAR(500)NULL플래그 설명 (운영자 입력). 선택 입력이라 NULL 허용
strategy_configTEXTNOT NULL평가 전략 JSON 문자열 (sealed EvaluationStrategy ↔ AttributeConverter). JSON 컬럼 금지 → TEXT (private-db-schema-convention). 전략 내부는 쿼리 대상 아님(방안 J)
versionBIGINTNOT NULL0낙관락(@Version) — 관리 API 동시 수정 lost-update 방지. BIGINT(Long) 확정 (senior-pm 정합): TDD ERD의 int 표기에서 조정, 레포 선례(payments.version·mcp_tokens.version BIGINT) 및 BE-01 @Version Long과 정합
created_atDATETIME(6)NOT NULL생성 시각 (UTC)
created_byBIGINTNULL생성자 user_id (ADMIN, SecurityAuditorAware). 물리 FK 없음
updated_atDATETIME(6)NOT NULL마지막 수정 시각
updated_byBIGINTNULL마지막 수정자 user_id. 물리 FK 없음
  • soft-delete 미도입: 라이프사이클은 status(ACTIVE↔ARCHIVED)로 완결한다. archive는 삭제가 아니라 평가 제외 상태 전이(User Scenario 9). deleted_at 없음 — 단순함 우선, partner 선례와 동일.
  • flag_type/status 값 집합은 애플리케이션 enum이 방어선. DB는 VARCHAR + COMMENT로 허용 값을 명시(CHECK 제약은 레포 선례 없음 → 미도입).

2. feature_flag_audit_logs — 변경 감사 이력 (V43, append-only)

컬럼타입NULLCOMMENT / 근거
idBIGINT AUTO_INCREMENT NOT NULLPK
flag_keyVARCHAR(100) NOT NULL대상 플래그 키 (feature_flags.flag_key 논리 참조, 물리 FK 없음). 조회 기준 컬럼. flag_id가 아닌 flag_key 보유 — TDD·조회 쿼리가 key 기반이고, 플래그 삭제(archive) 후에도 이력 독립 보존
change_typeVARCHAR(20) NOT NULL변경 종류: CREATED | UPDATED | ARCHIVED | ACTIVATED (ENUM 금지 → VARCHAR)
actor_user_idBIGINT NOT NULL변경자 user_id (SecurityContext principal.id). 물리 FK 없음
before_snapshotTEXT NULL변경 전 값 JSON(FeatureFlagSnapshot ↔ Converter). CREATED는 before 없음 → NULL. JSON 금지 → TEXT
after_snapshotTEXT NOT NULL변경 후 값 JSON(FeatureFlagSnapshot). 모든 변경에 존재
occurred_atDATETIME(6) NOT NULL변경 발생 시각 (UTC). 조회·정렬 기준. append-only라 적재 시각과 동일 — 별도 created_at 미도입(중복)
  • append-only: updated_*/deleted_* 없음. 감사 로그는 불변(mcp_audit_logs 선례).
  • 스냅샷 TEXT 채택 근거: 이력은 “그 시점의 전체 상태 사본”이라 정규화 이득이 없다(전략 내부 컬럼별 조회 요건 없음). 정규화(방안 K)는 자식 테이블·조인 비용만 늘린다 → TEXT.
  • created_by/created_at 별도 미도입: actor_user_id = 행위자, occurred_at = 적재 시각으로 append-only 감사에 충분. 컬럼 중복 제거.

쿼리 패턴 → 인덱스 매핑

전제 (중요): 평가 트래픽(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_keyEquality(선두, 특정 플래그로 필터 — 고선택도), occurred_atSort(정렬·범위, 두 번째). 커버링 정렬로 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.51KBstrategy_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건
행 크기~1KBbefore/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개 도메인·스키마 무변경. 스키마가 코드보다 먼저 배포돼도 안전(신규 테이블은 아무도 참조 안 함).

단계작업배포 순서락 영향롤백 지점
1. 스키마 (먼저)V43__create_feature_flags.sqlfeature_flags + feature_flag_audit_logs 2개 테이블 CREATE TABLE (2 테이블 = 1 파일, TDD 권장)스키마 먼저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 기준 워크트리 필수.

자가 점검 (TDD 도메인 모델 ↔ 컬럼 대응)

TDD 필드/요구대응 컬럼확인
flag_key UNIQUEfeature_flags.flag_key + uq_feature_flags_flag_key
type 4종 (VARCHAR)flag_type VARCHAR(32)✅ ENUM 금지 준수
status ACTIVE/ARCHIVED (VARCHAR)status VARCHAR(20)
strategy_config TEXT (JSON 문자열)strategy_config TEXT✅ JSON 컬럼 금지 준수
@Version 낙관락version BIGINT✅ BIGINT(Long) 확정 (senior-pm 정합, BE-01 @Version Long)
DATETIME(6)모든 시각 컬럼
created/updated_by (FK 금지)created_by/updated_by BIGINT 일반 컬럼✅ FK 금지 준수
감사: flag_key·변경자·대상·이전→이후·occurred_ataudit 테이블 전 컬럼
(flag_key, occurred_at) 복합 인덱스idx_feature_flag_audit_logs_flag_key_occurred_at✅ ESR 순서 근거 명시
감사 1년 보존보존 정책 섹션 (삭제 없이 유지 권고 + 배치 옵션)
모든 컬럼·테이블 COMMENT정의 표의 COMMENT 열✅ (mysql-implementer가 DDL에 반영)
BOOLEAN 미사용strategy 내부 enabled는 TEXT JSON 내부✅ TINYINT 불요
  • 컨벤션 위반 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 번호 잠정 배정
2026-07-03senior-pm 정합 반영 — version BIGINT(Long)·status 인덱스 미채택을 확정으로 못박음. tickets/DB-01-create-feature-flags.md 신설(BE-02의 DB-01 유령 참조 해소)