콘텐츠로 이동
테넌트 기본 정책과 병원별 재정의를 비교하고 예약에 적용할 정책을 확인하는 로봇
같은 예약 서비스라도 병원별 정책을 합성한 결과가 달라질 수 있습니다.

같은 요청인데 왜 병원마다 결과가 다를까

섹션 제목: “같은 요청인데 왜 병원마다 결과가 다를까”

환자가 같은 상품을 선택하고 같은 시간대를 요청해도 결과가 병원마다 다를 수 있습니다. A 병원은 상담원의 확인을 거쳐야 예약을 확정하고, B 병원은 이미 검증된 조건이면 바로 확정할 수 있습니다. 이것은 예약 생성 코드가 병원 이름을 보고 예외를 고르는 문제가 아닙니다. 예약에 적용할 정책이 테넌트 기본 정책과 병원별 재정의를 합성한 결과이기 때문입니다.

이 차이를 설명하려면 다음 네 가지를 분리해야 합니다.

구분답해야 하는 질문
범위이 규칙은 모든 병원에 적용되는가, 특정 병원에만 적용되는가?
선택병원 설정이 기본값을 그대로 상속하는가, 값을 바꾸는가, 선택 기능을 끄는가?
시점예약 요청을 판단한 시점과 실제 방문이 이루어질 시점은 언제인가?
이력나중에 정책이 바뀌어도 당시 적용한 정책 버전과 판단 근거를 확인할 수 있는가?

정책을 설정 테이블로만 다루면 마지막 두 질문에 답하기 어렵습니다. 이번 설계는 정책을 버전이 있는 계약으로 저장하고, 테넌트 기본 정책과 병원별 재정의를 합성해 예약 판단에 사용할 적용 정책을 만듭니다. 소스의 EffectiveSchedulingPolicy가 이 적용 정책을 나타냅니다.

기본 정책과 병원별 재정의는 합성 규칙이다

섹션 제목: “기본 정책과 병원별 재정의는 합성 규칙이다”

정책의 적용 범위는 두 층으로 나뉩니다.

범위역할
테넌트 기본 정책해당 테넌트의 모든 병원이 사용할 기준기본 승인 방식, 안전 상한, 운영 시간
병원별 재정의특정 병원의 업무 차이를 표현특정 기능의 추가 확인, 선택 기능 비활성화

병원별 재정의는 세 가지 의도로 표현합니다.

의도의미결과
INHERIT테넌트 기본 정책을 그대로 사용테넌트 값을 적용 정책에 반영
SET병원에 다른 값을 지정병원 정책을 해당 값의 출처로 기록
DISABLE선택 기능만 사용하지 않음사용하지 않는 기능을 별도로 기록

INHERITnull과 같은 뜻으로 취급하면 안 됩니다. null은 “값을 아직 모른다”는 뜻일 수 있지만, INHERIT는 “테넌트 값을 의도적으로 따른다”는 명시적인 결정입니다. 반대로 안전 상한이나 필수 정책은 DISABLE로 끄거나 기본값보다 느슨하게 바꿀 수 없습니다.

테넌트 기본 정책과 병원별 재정의를 합성하고 활성화 검사를 거쳐 예약에 사용할 적용 정책을 만드는 흐름
테넌트 기본 정책과 병원별 재정의는 명시적인 합성 단계를 거쳐 하나의 적용 정책이 됩니다.

같은 규칙을 코드 곳곳에서 다시 해석하지 않고 합성 단계를 한 곳에 모으면, “이 병원은 왜 다르게 처리되었나?”라는 운영 질문에 답할 수 있습니다. 실제 구현도 정책 데이터 형식(payload)에서 INHERIT, SET, DISABLE을 구분하고, SchedulingPolicyCompiler가 각 값의 출처를 남깁니다.

정책은 값을 바꾸는 설정이 아니라 버전이 있는 계약이다

섹션 제목: “정책은 값을 바꾸는 설정이 아니라 버전이 있는 계약이다”

운영자가 화면에서 숫자나 옵션을 바꿨다고 해서 그 값이 즉시 모든 예약에 적용되면 안 됩니다. 변경 전에는 내용을 검증하고 영향을 미리 확인해야 하며, 승인과 적용 시점을 기록해야 합니다. 활성화에 실패했을 때는 기존 정책도 유지해야 합니다.

정책의 생명주기는 다음처럼 제한합니다.

DRAFT ──▶ SCHEDULED ──▶ ACTIVE ──▶ RETIRED
└──────────────▶ ACTIVE
└────────────────────────────▶ RETIRED
단계의미운영자가 확인할 것
DRAFT아직 적용하지 않은 정책필수 항목과 안전 상한을 만족하는가?
SCHEDULED미래 시점에 적용할 정책적용 시점과 예상되는 영향이 분명한가?
ACTIVE현재 읽기에 사용할 정책기대한 세대와 버전으로 활성화되었는가?
RETIRED더 이상 새 판단에 쓰지 않는 정책과거 이력을 추적할 수 있는가?

초안 작성, 검증, 미리보기, 승인, 활성화 예약 또는 즉시 활성화, 폐기는 서로 다른 단계입니다. 특히 승인은 상태 전이를 뜻하지 않습니다. 승인된 내용이라도 적용 시점에 세대가 바뀌었거나 다른 운영자가 먼저 다른 버전을 활성화했다면 다시 확인해야 합니다.

적용 정책과 정책 버전이 당시의 결정을 설명한다

섹션 제목: “적용 정책과 정책 버전이 당시의 결정을 설명한다”

예약 요청을 판단할 때 필요한 것은 “현재 병원 설정”이 아닙니다. 판단 시점과 실제 방문 시점에 적용할 정책, 그리고 그 정책을 구성한 버전과 출처가 필요합니다. EffectiveSchedulingPolicy는 다음 정보를 하나의 불변 객체로 묶습니다.

필드역할
decisionAt예약 명령을 판단한 시점
serviceAt실제 방문에 적용할 정책을 판단하는 시점
generation함께 합성한 테넌트 정책과 병원 정책의 세대
sourceVersions각 정책 종류에 사용한 테넌트·병원 정책 버전
sourceByPath각 값이 기본 정책에서 왔는지 병원 재정의에서 왔는지
snapshotHash정책 합성 결과가 같은지 확인하는 해시

핵심 필드만 보면 다음과 같습니다.

data class EffectiveSchedulingPolicy(
val decisionAt: Instant,
val serviceAt: Instant,
val generation: PolicyGenerationVector,
val sourceVersions: Map<SchedulingPolicyKind, SourceVersion>,
val sourceByPath: Map<String, PolicyValueSource>,
val snapshotHash: String,
)

SchedulingPolicyCompilerserviceAtdecisionAt보다 앞서지 않는지도 검사합니다. 예약 생성 경로가 연결된 뒤에는 예약 요청, 미리보기, 운영 화면이 같은 적용 정책을 참조할 수 있습니다. 정책 버전과 snapshotHash를 함께 남기는 이유는 정책을 자동으로 예약에 적용하기 위해서가 아니라, 어떤 정책을 판단 근거로 사용했는지 나중에 검증하기 위해서입니다.

활성화가 충돌하면 기존 정책을 유지한다

섹션 제목: “활성화가 충돌하면 기존 정책을 유지한다”

두 운영자가 같은 병원의 정책을 동시에 활성화할 수 있습니다. 마지막 요청이 무조건 성공하면 먼저 승인된 변경이 조용히 덮어써집니다. 이를 막기 위해 활성화 요청에 직전에 읽은 범위 버전인 expectedScopeRevision과 정책 세대인 generation을 함께 보냅니다. 저장 직전에 두 값이 현재 값과 일치할 때만 정책을 바꾸는 CAS(Compare-And-Set)를 사용합니다.

충돌이 나면 새 정책을 억지로 적용하지 않고 다음을 보장합니다.

  • 기대한 범위 버전이나 정책 세대가 아니면 활성화를 거부한다.
  • 같은 Idempotency-Key로 재시도한 요청은 처음 처리한 결과를 반환한다.
  • 예약된 활성화가 허용된 지연 시간을 넘기면 실패로 처리하고 기존 ACTIVE 정책을 유지한다.
  • 운영자는 충돌 원인을 확인한 뒤 새 세대를 기준으로 영향을 다시 미리 확인하고 승인한다.

버튼을 눌렀다는 사실만으로 정책이 바뀌었다고 판단해서는 안 됩니다. 운영자는 활성화 요청의 결과를 확인한 뒤 적용 정책을 다시 조회해야 실제로 어떤 정책이 선택됐는지 알 수 있습니다.

정책이 바뀌어도 기존 확정 예약은 변경하지 않는다

섹션 제목: “정책이 바뀌어도 기존 확정 예약은 변경하지 않는다”

정책 변경의 적용 범위는 미래의 판단으로 제한합니다. 이미 환자에게 확정한 예약을 현재 정책으로 다시 계산하면, 시스템이 환자와 합의한 예약을 사후에 바꾸게 됩니다.

기존 상태정책 변경 시 기본 처리
PROPOSED새 정책으로 다시 계산할 수 있음
HELD보류가 만료된 뒤 다음 판단부터 새 정책을 적용할 수 있음
CONFIRMED기존 확정 예약을 보호하고 자동으로 변경하지 않음

확정된 방문 시간이나 가격을 바꿔야 한다면 기존 예약을 덮어쓰는 대신 새 제안을 만들고 환자의 동의를 다시 받아야 합니다. 적용 정책의 버전 정보와 확정 예약을 서로 다른 기록으로 보존하는 이유가 여기에 있습니다. 정책은 앞으로 내릴 판단의 규칙이고, 확정 예약은 환자와 이미 합의한 결과입니다.

현재 구현과 아직 연결하지 않은 경계

섹션 제목: “현재 구현과 아직 연결하지 않은 경계”

이번 기반 설계는 정책을 작성하고 검증하며, 적용 정책을 조회하고, 새 정책 버전을 안전하게 활성화하는 경계를 마련합니다. 예약 생성 경로가 이 적용 정책을 실제 예약 판단에 사용하는 작업은 별도의 연결 단계입니다. 기능 플래그는 다음 순서로 나뉘며 기본값은 모두 꺼져 있습니다.

  1. 백그라운드 정책 컴파일 검증(shadow compile)
  2. 적용 정책 조회(effective read)
  3. 관리자 정책 변경(admin write)
  4. 영향도 미리보기 워커(preview worker)
  5. 예약된 정책 활성화(scheduled activation)

이 순서를 지키면 저장한 정책이 기존 예약 흐름에 갑자기 영향을 주지 않는지 단계별로 확인할 수 있습니다. “정책 데이터를 저장하고 조회할 수 있다”와 “예약 결과가 정책에 따라 달라진다”는 서로 다른 출시 단계입니다.

병원마다 예약 결과가 다른 이유는 코드가 병원 이름을 보고 예외를 선택하기 때문이 아닙니다. 테넌트 기본 정책과 병원별 재정의를 명시적으로 합성하고, 그 결과를 세대·출처·정책 버전·해시가 있는 적용 정책으로 만들기 때문입니다.

이 구조가 지키는 경계는 분명합니다.

  • INHERIT, SET, DISABLE은 서로 다른 의도다.
  • 정책 변경은 버전과 생명주기를 가진다.
  • 활성화가 충돌하면 CAS와 세대 검사로 기존 정책을 보호한다.
  • 기존 CONFIRMED 예약은 미래 정책으로 변경하지 않는다.

다음 글에서는 이 정책 경계와 연결되는 대기 목록을 살펴봅니다. 빈시간이 생겼을 때 누구에게 어떤 제안을 만들 수 있는지는 정책을 읽는 시점과 확정 예약을 보호하는 규칙 위에서 결정됩니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.