[BE-01] FeatureFlag 도메인 코어 계약 (선행 병목)
작업 내용 (설계 의도)
변경 사항
신규 featureflag 도메인의 연관 병목(공통 계약)을 한 wave에 확립한다. 후행 티켓 전부(BE-02 영속화·BE-03 캐시/pubsub·BE-04 도메인서비스·BE-05 애플리케이션·BE-06 평가클라이언트·BE-09 데모)가 import하는 도메인 코어·전략·공용 평가 인터페이스를 한 책임으로 묶어 직렬 사슬을 방지한다. 근거 TDD: ../TDD.md “시스템 역할 경계”·“인터페이스 시그니처”·“도메인 바운디드 컨텍스트 판단”.
FeatureFlag(Rich Domain @Entity, 단일 클래스 — 레포 관례): private 생성자 +create()정적 팩토리 +private var필드. JPA 하이드레이션은 no-arg 생성자(reconstitute 미사용, 레포 100% 단일클래스 관례 준수). 필드: flagKey·type·status·strategy·description·version: Long(@Version, DB BIGINT — 레포 선례·senior-dba 권고)·감사시각.strategy는@Type(JsonStringType::class)+ TEXT 컬럼으로 sealedEvaluationStrategy를 직접 매핑(도메인→infra import 없음, AttributeConverter 미사용).- 메서드:
create()(flagKey 형식·strategy 유효성 검증),evaluate(context)(ARCHIVED면 평가 제외 신호, 아니면strategy.evaluate위임),updateStrategy()·archive()·activate()(전이 검증 + 변경 이벤트 register),toSnapshot(). 시각은 메서드 내부ZonedDateTime.now()해결(시간 인자 금지). FeatureFlagTypeenum: RELEASE/OPERATIONAL/EXPERIMENT/ENTITLEMENT —VariantBucketing전략은 EXPERIMENT에서만 허용 검증.FeatureFlagStatusenum: ACTIVE/ARCHIVED +canTransitTo(next)캡슐화.- sealed
EvaluationStrategy+evaluate(context): FeatureEvaluation다형성:GlobalToggle(enabled)·PercentageRollout(percentage 0..100)·AttributeMatch(attribute,value)·VariantBucketing(variants≤4, weight합100).Variant(name,weight). - sealed
FeatureEvaluation:On/Off/Assigned(variantName). StableBucketer(object, 순수):bucket(flagKey, userId): Int=Math.floorMod(murmur3_32("$flagKey:$userId"),100)(0..99). Unleash stickiness 참조, flagKey를 salt로.FeatureFlagAuditLog(@Entity, 단일클래스):create(changeType, actorUserId, before?, after). before/after는@Type(JsonStringType::class)TEXT로FeatureFlagSnapshot매핑.FeatureFlagSnapshot(data class): key·type·status·strategy·description (raw String 금지, 타입화).- 도메인 인터페이스:
FeatureFlagRepository·FeatureFlagAuditLogRepository(repository),FeatureFlagCacheStore·FeatureFlagChangeBroadcaster(gateway) — 시그니처는 TDD “인터페이스 시그니처” 그대로. 감사 조회는findByFlagKey(key, pageable: Pageable): Page<FeatureFlagAuditLog>(레포 McpAuditLogRepository 선례, occurred_at desc — total 포함 응답용). - 도메인 이벤트:
FeatureFlagChangedEvent(AbstractDomainEvent, topic=null → Spring 내부 전파). AggregateRoot domainEvents에 register. - 예외:
FeatureFlagNotFoundException·DuplicateFeatureFlagKeyException·InvalidEvaluationStrategyException·FeatureFlagStatusConflictException(모두BusinessException확장, 적절한ErrorStatus). - 공용 평가 계약(common):
domain/common/FeatureFlagEvaluator(interface:isEnabled/variant) +domain/common/FeatureContext(VO: userId?·attributes). 다른 도메인이domain.featureflag를 import하지 않고 평가하도록 common에 배치(선례: DistributedLock·PermissionRepository). domain/common/BusinessException.kt의ErrorStatusenum에SERVICE_UNAVAILABLE(503)추가 — 데모 게이팅용. 기존GlobalExceptionHandler#handleBusinessException이 자동 매핑(핸들러 변경 불필요).
의존
- 없음 (선행 병목)
다이어그램
클래스 의존
flowchart LR FeatureFlag --> EvaluationStrategy FeatureFlag --> FeatureFlagStatus FeatureFlag --> FeatureFlagChangedEvent FeatureFlag --> AggregateRoot["common.AggregateRoot"] EvaluationStrategy --> FeatureEvaluation EvaluationStrategy --> StableBucketer EvaluationStrategy --> FeatureContext["common.FeatureContext"] FeatureFlagRepository -.->|반환| FeatureFlag Evaluator["common.FeatureFlagEvaluator"] --> FeatureContext
테스트 케이스
- GlobalToggle(enabled=true) 평가는 On, false는 Off를 반환한다
- PercentageRollout(50)에서 동일 userId를 반복 평가하면 항상 같은 판정을 받는다(sticky)
- 서로 다른 flagKey는 같은 userId·같은 percentage라도 버킷이 독립적으로 분포한다(salt 효과)
- AttributeMatch(plan,PREMIUM)는 context.attributes에 plan=PREMIUM일 때만 On이다
- VariantBucketing 2개(50:50)에서 동일 userId는 항상 동일 variant(Assigned)를 받는다
- PercentageRollout·VariantBucketing 평가 시 userId가 null이면 Off를 반환한다
- create는 percentage>100·variant>4개·weight합≠100·EXPERIMENT 아닌데 VariantBucketing이면 예외를 던진다
- ACTIVE→ARCHIVED는 허용되고 ARCHIVED→ARCHIVED는 canTransitTo가 거부한다
- archive()·activate()·updateStrategy() 호출 시 FeatureFlagChangedEvent가 domainEvents에 적재된다
- ARCHIVED 플래그의 evaluate는 전략 평가를 수행하지 않는다(평가 제외 신호)
- StableBucketer.bucket은 0..99 범위를 벗어나지 않는다(음수 해시 floorMod 처리)