범위 내 편집
tenant baseline을 기준으로 clinic override를 작성한다.
CLINIC APPOINTMENT · POLICY FOUNDATION
tenant 기본값과 clinic override를 typed policy로 관리하고, Gateway 인증정보를 ActorContext로 고정하며, 의사결정 당시의 유효 스냅숏을 영구 보존한다.
SIMULATION · DEFAULT VIEW
병원 운영자는 override를 바로 활성 설정으로 쓰지 않는다. compiler와 preview로 영향 범위를 확인하고, 승인과 CAS를 통과한 수정불가 스냅숏만 새 예약 의사결정에 사용한다.
tenant baseline을 기준으로 clinic override를 작성한다.
typed payload를 병합하고 완화할 수 없는 안전 규칙을 확인한다.
대상 예약과 제약 변화를 범위 제한 sample과 지표로 보여준다.
필요한 승인 수와 actor scope를 만족하고 stale draft를 거절한다.
CAS로 generation을 올리고 수정불가한 유효 스냅숏과 outbox를 함께 기록한다.
항상 신뢰된 scope head의 최신 generation과 hash를 사용한다.
만료까지 기존 정책 스냅숏을 보호한다.
정책 활성화가 기존 고객 약속을 조용히 취소하거나 재작성하지 않는다.
activation event는 eviction 최적화일 뿐이다. effective read는 DB scope-head generation을 확인하며 DB 장애 시 오래된 스냅숏을 성공처럼 반환하지 않는다.
HISTORY · DECISION PROVENANCE
이 HTML은 이해를 돕는 시각 동반 문서이며, 상세 계약과 인수 기준은 Markdown이 규범이다.
Issue #182가 병원별 예약 정책을 재현 가능하게 만들 요구를 고정했다.
규범 Markdown이 생명주기, approval, compiler, generation, cache, 오류 의미론을 정의했다.
시각 동반 문서 설계와 구현 계획이 provenance와 게시 계약을 고정했다.
정책을 controller 분기나 nullable 설정으로 흩뜨리면 과거 결정을 설명할 수 없고, 동시 활성화와 병원별 override가 충돌한다.
SQL 무결성은 강하지만 kind마다 migration과 compiler branch가 반복된다.
확장은 쉽지만 lifecycle, revision, interval 유일성과 타입 검증이 약하다.
공통 동시성·감사는 정규화하고 payload는 Kotlin sealed type으로 검증한다.
새 정책은 앞으로의 작업에만 적용한다. 이미 진행된 예약, hold, 확정 약속과 그 판단 근거는 수정하지 않는다.
Gateway는 인증하고 예약서비스는 업무 권한을 판단한다.
request body의 actor type, role, tenant, clinic, patient, booking origin과 일반 X-User-* header는 권한 근거가 아니다.
actor ID/type, issuer, token ID, 인증 시각, correlation ID, tenant/clinic scope.
Bearer token 원문, 전체 JWT claims, parser detail, raw idempotency key, 불필요한 환자 개인정보.
현재 JWT 구현의 issuer/HMAC 검증을 required jti, audience, algorithm allowlist, assurance, actor type, patient subject, 복수 clinic scope까지 확장한다. 모순된 claim은 fail closed하며 실패 응답·로그는 token/claim/parser detail을 redaction한다.
예약 생성 출처는 body parameter가 아니라 ActorContext에서 파생한다. 이번 foundation은 규칙을 compile하고 실제 예약 상태 머신은 다음 issue가 구현한다.
ADMIN|STAFF → scope·capacity·동의 증빙 검증 → 정책이 허용하면 CONFIRMED
PATIENT → own-patient scope 검증 → PROVISIONAL → 관리자 승인 → CONFIRMED
요청만 기록한다. 자원을 점유하지 않는다.
운영 후보로 표시하지만 충돌 방지 점유는 아니다.
제한된 TTL 동안 실제 자원을 점유한다.
가예약 요청 TTL 기본값은 24시간이며 5분~7일 범위다. HARD_HOLD만 1~30분의 별도 resource hold TTL이 필수이고 request TTL을 넘을 수 없다.
tenant, scope, clinic, kind, version, schema version, lifecycle, effective interval, revision, payload hash, actor audit.
BOOKING_COMMITMENT, hold/consent, capacity, reliability, reconfirmation, disruption, extension, SLA.
INHERIT, SET(value), DISABLE을 명시한다. clinic은 platform·tenant hard ceiling을 완화할 수 없다.
동일 logical input은 map 순서와 관계없이 같은 스냅숏 hash를 만든다. 각 값의 출처와 policy version을 함께 보존한다.
DECISION_TIME은 hold·consent·proposal, SERVICE_TIME은 capacity·reconfirm·SLA에 사용한다.
{tenantGeneration, clinicGeneration}으로 clinic 변경이 tenant 전체 cache를 불필요하게 무효화하지 않게 한다.
clinic override generation이 증가하면 같은 transaction에서 tenant head의 clinicGenerationEpoch도 증가한다. preview는 SHA-256(tenantId:epoch)를 고정하고 매 page마다 unique scope-head 한 행만 읽는다. 병원·예약 목록은 impact scan 입력이며 policy generation에는 포함하지 않는다.
cache bucket은 effective interval, scheduled activation, emergency expiration, clinic timezone의 DST gap/overlap 경계를 넘지 않는다.
decisionAt과 serviceAt은 offset 포함 RFC 3339 필수 query다. server-now default와 local date-time은 허용하지 않으며 두 값을 Instant로 정규화해 응답에 그대로 표시한다.
DB time due scan, lease CAS, deterministic idempotency key, 범위 제한 retry와 startup catch-up을 사용한다. 60초 지연은 warning, 5분 지연은 MISSED critical이며 이전 active를 유지한 채 manual replay 또는 retire한다.
| Table | 책임 | 핵심 invariant |
|---|---|---|
scheduling_policy_definitions | 공통 envelope와 typed canonical payload | scope/version 유일성, 불변 published payload |
scheduling_policy_approvals | draft revision별 승인 evidence | stale revision과 동일 actor 이중 승인 거절 |
scheduling_policy_scope_heads | scope-wide revision·generation과 tenant clinic epoch | tenant→clinic 잠금 순서, clinic generation과 tenant epoch의 원자적 증가 |
effective_scheduling_policy_snapshots | compiled payload와 source map | 불변 hash와 tenant/clinic 경계 |
scheduling_policy_activation_commands | keyed idempotency hash·due runner lease·retry | 동일 hash+fingerprint 재현, raw key 비보존, crash catch-up, MISSED 가시성 |
scheduling_policy_preview_jobs | bounded async preview | cursor·revision·generation·deadline checkpoint |
scheduling_outbox_events | generic aggregate event | V9 nullable aggregate 열 + nullable legacy planId, backfill/dual-write; 운영 parity 후 별도 V10 aggregate NOT NULL cutover |
| Stable code | HTTP | Retryable | 의미 | Caller action |
|---|---|---|---|---|
POLICY_PAYLOAD_INVALID | 400 | false | typed validation 실패 | payload 수정 |
POLICY_OVERRIDE_FORBIDDEN | 400 | false | hard ceiling 완화 또는 필수값 disable | override 수정 |
POLICY_ACTOR_FORBIDDEN | 403 | false | actor scope 또는 명령 권한 없음 | 권한·대상 확인 |
POLICY_RESOURCE_NOT_FOUND | 404 | false | path scope와 resource 불일치 또는 없음 | tenant·clinic·ID 확인 |
POLICY_DRAFT_STALE | 409 | false | expected revision 불일치 | 최신 draft 재조회 |
POLICY_PREVIEW_STALE | 409 | false | preview revision/generation 불일치 | preview 재실행 |
POLICY_ACTIVATION_CONFLICT | 409 | false | head CAS 또는 interval 충돌 | 최신 active head 재조회 |
POLICY_IDEMPOTENCY_CONFLICT | 409 | false | 같은 key의 다른 명령 | 의도 확인 후 새 key 사용 |
POLICY_APPROVAL_INSUFFICIENT | 422 | false | 승인 수 또는 assurance 부족 | 필요한 승인 수집 |
POLICY_PREVIEW_LIMITED | 429 | true | tenant async queue 포화 | Retry-After 뒤 같은 요청 재시도 |
POLICY_ACTIVATION_MISSED | 409 | false | scheduled deadline 경과 | manual replay 또는 retire |
모든 오류는 HTTP status, stable code, retryable, correlation ID와 caller/operator action을 제공한다. 같은 요청의 자동 재시도는 POLICY_PREVIEW_LIMITED만 허용한다. Preview는 동기 완료 시 200, async 전환 시 202와 jobId·Location·Retry-After를 반환하며 job endpoint에서 terminal status를 조회한다.
activation 결과·lateness·missed, compile latency, 신뢰된 generation read, stale cache rejection, preview deadline, outbox 상태를 metric과 structured log로 남긴다.
feature flag off → shadow compile → effective read/admin API → 후속 consumer 순으로 활성화한다.
activation event는 eviction 최적화다. 모든 effective read는 DB scope head generation을 확인하며 DB 장애 시 stale 스냅숏을 반환하지 않는다.
이전 compatible version을 새 activation으로 복구하고 스냅숏은 수정하지 않는다. destructive down migration과 DB 직접 수정은 금지한다.
scheduled lateness 60초/5분, outbox pending 60초·failed 지속, preview stale/deadline 5%, activation conflict 5%, 신뢰된 generation read failure 1%를 warning/critical 기준으로 삼고 tenant별 조정한다.
주석과 KDoc은 코드 문장을 반복하지 않는다. transaction 소유권, 상태 전이 거절 이유, CAS가 보호하는 invariant, hash 규칙, caller/operator action처럼 “왜 필요한가”를 설명한다.
typed policy, ActorContext, compiler, 스냅숏, 생명주기, approval, generation, preview, cache, generic outbox, admin API.
PROVISIONAL·HELD·CONFIRMED, 고객 동의, resource allocation, waitlist, disruption, overbooking.
정책 활성화는 이미 진행된 시술·확정 예약·유효 hold를 조용히 취소하거나 재작성하지 않는다. 실제 안전 위반은 정책 변경이 아니라 disruption workflow로 처리한다.