콘텐츠로 이동

[설계 1] 상품이 바뀌어도 고객의 예약은 변경하지 않는다: 상품 버전과 구매 기준 정보

상품 v1·v2 카드와 구매 기준 정보, 예약 계획 리비전, 확정 예약을 시간축으로 연결하는 작은 로봇 작업자들
상품을 새로 발행하는 일과 이미 구매한 환자의 예약을 바꾸는 일은 같은 작업이 아닙니다. 구매 시점의 계약과 이미 확인된 사실을 먼저 보호해야 합니다.

상품 개발부가 상품을 바꾸는 순간, 예약 서비스에는 달력보다 먼저 답해야 할 질문이 생깁니다.

이미 상품을 구매한 환자에게 새 상품 정의를 그대로 적용해도 되는가?

환자 A가 한 병원에서 이벤트 상품, N회 방문 상품, 패키지 상품을 차례로 구매했다고 해보겠습니다. 상품팀이 같은 상품 계보를 v2로 발행하면 신규 구매에는 새 정의를 적용할 수 있습니다. 하지만 A가 이미 구매한 v1의 치료 항목과 남은 회차까지 현재 카탈로그로 다시 전개하면, 예약 서비스가 고객의 구매 계약을 조용히 바꾸게 됩니다.

이번 글의 핵심은 멱등성 구현이 아닙니다. 상품 버전이 바뀌었을 때 무엇을 그대로 보존하고, 무엇만 협의할 수 있는지를 환자 A의 시간축으로 설명합니다. 중복 전달된 구매 이벤트는 후반부의 짧은 안전장치로만 다룹니다.

환자 A의 시간축: 상품을 샀다는 사실과 예약

섹션 제목: “환자 A의 시간축: 상품을 샀다는 사실과 예약”

환자 A의 상품명·금액·병원명은 공개하지 않고, 예약에 영향을 주는 업무 의미만 남깁니다.

시점환자 A의 행위 또는 이벤트상품·구매의 의미예약 서비스가 보존하는 것
T1이벤트 상품을 구매한다기간 안에 한 번 사용할 권리sourcePurchaseId와 구매 당시 카탈로그 v1
T2N회 상품을 추가 구매한다여러 회차로 이행할 의무회차별 PlannedTreatment와 반복 간격
T3패키지 상품을 구매한다여러 항목·선후 조건·자원을 가진 상품 BOM항목별 버전 출처 이력과 의존성
T4일부 항목을 완료하고 일부는 미래로 남긴다과거 사실과 미진행 의무가 갈라짐완료 사실, 남은 항목, 현재 Plan 리비전
T5상품팀이 카탈로그 v2를 발행한다이후 구매의 계약이 변경됨v1 기준 정보와 v2 프로젝션을 함께 보존
T6기존 구매 전환표와 동의가 도착한다예외적으로 미래 의무를 바꿀 수 있음새 리비전과 전환·동의 증거
T7전환으로 일정 변경이 필요하거나 A가 거부한다상품 변경과 예약 변경은 별도 업무확정 예약 보호와 CRM/상담 전달

세 구매를 하나의 예약 행으로 합치지 않는 이유가 여기에 있습니다. 이벤트 상품의 한 번, N회 상품의 세 번째 회차, 패키지의 특정 항목은 남은 의무와 선후 조건이 다릅니다. 같은 방문에서 여러 상품의 항목을 처리할 수는 있지만, 그 방문에 합의된 AppointmentItem과 각 구매의 Plan은 별도로 추적해야 합니다.

구매는 무엇을 사용할 권리를 샀는지에 대한 상업적 사실입니다. Plan은 그 구매를 어떤 실행 의무로 해석했는지에 대한 예약 서비스의 구매 기준 정보입니다. 확정 예약은 특정 시간·항목·자원으로 이행하기로 합의한 예약입니다. 세 개를 같은 레코드로 취급하면 상품 변경이 과거 예약을 덮어쓰게 됩니다.

상품 버전은 판매 문구가 아니라 구매 시점의 계약이다

섹션 제목: “상품 버전은 판매 문구가 아니라 구매 시점의 계약이다”

예약 서비스가 보관하는 상품 프로젝션은 상품 원본의 복사본을 임의로 수정한 데이터가 아닙니다. 현재 ProductCatalogDefinition은 상품 원본 서비스가 발행한 catalogVersion, BOM 항목, 반복 횟수, 진료 시간, 자원 역량, 선후 의존성을 한 버전의 불변 정의로 취급합니다. ACTIVE 프로젝션은 새 Plan 생성에 사용할 수 있고, RETIRED 프로젝션은 과거 Plan을 읽을 때만 남습니다.

구매 완료 이벤트에는 가격표 전체를 다시 싣지 않아도 됩니다. 예약 서비스가 재현해야 하는 최소 입력은 PurchaseCompletedEventsourcePurchaseAuthority, sourcePurchaseId, productId, catalogVersion, bookingPreference 같은 값입니다. Plan을 만들 때는 구매 원본 서비스와 환자 참조, 구매 당시 카탈로그 payload 해시를 연결합니다.

AppointmentPlanFactory는 카탈로그의 각 BOM 항목을 repeatCount만큼 PlannedTreatment으로 확장하고 의존성을 보존합니다. 이 단계는 예약 일시를 확정하지 않습니다. 고객이 희망 일정을 제공하지 않았다는 사실을 자동 확정으로 바꾸지도 않습니다.

따라서 Plan과 이를 바탕으로 만든 예약 흐름에는 최소한 다음 시간축이 남아야 합니다.

  • 어떤 sourcePurchaseId와 구매 원본 서비스가 이 계획을 만들었는가
  • 어떤 productIdcatalogVersion을 사용했는가
  • 어떤 payload 해시와 BOM 항목·반복·의존성을 해석했는가
  • 어떤 bookingPreference를 구매와 함께 전달했는가
  • 어떤 제안에 어떤 정책을 적용했고, 이후 리비전과 확정 예약이 어느 Plan에 속하는가

이 기준 정보는 고객 화면에 보이는 가격표를 복사하는 기능이 아닙니다. 상품 원본은 상품 개발부와 커머스가 소유하고, 예약 서비스는 그 구매의 예약 의무를 다시 계산할 수 있는 최소 계약을 보존합니다.

이벤트·N회·패키지는 각각 다른 Plan이 된다

섹션 제목: “이벤트·N회·패키지는 각각 다른 Plan이 된다”

상품 유형을 달력의 예약 횟수로만 표현하면 업무 규칙이 사라집니다.

상품 유형Plan으로 남기는 것예약할 때 묻는 질문
이벤트 상품repeatCount가 1인 PlannedTreatment, 유효 기간, 예약 규칙이 혜택을 사용할 수 있는 날짜와 자원이 있는가?
N회 상품회차별 PlannedTreatment, 최소·선호·최대 간격이전 회차와 필요한 간격을 만족했는가? 남은 회차는 몇 개인가?
패키지 상품항목별 상품 BOM, 선택 결과, 의존성과 방문 그룹화같은 방문에 묶을 항목과 다음 방문으로 미룰 항목은 무엇인가?

패키지는 특히 “상품 하나 = 예약 하나”로 축약하기 어렵습니다. 현재 PackageExecutionPlanner는 외부에서 전개한 정확한 구성 요소 버전과 실행 항목, 의존 관계, 방문 그룹화 조건을 검증합니다. 선택 항목의 출처 정보가 빠졌거나 의존성 그래프에 cycle이 있으면 일부를 잘라 성공으로 만들지 않고 전체를 거부합니다.

구매 완료 이벤트
└─> 구매별 AppointmentPlan
├─ 이벤트 상품: PlannedTreatment 1개
├─ N회 상품: PlannedTreatment 1..N
└─ 패키지: 항목 + 의존성 DAG + 방문 그룹화

이 구조는 “세 상품을 샀으니 앞으로 가능한 시간을 모두 잡아 두자”는 오해도 막습니다. Plan은 이행해야 할 의무를 보존하지만, 예약 서비스가 미래의 모든 자원을 미리 점유한다는 뜻은 아닙니다. 실제 시간·자원·동의가 합의된 항목만 확정 예약으로 이어집니다.

상품 v2가 발행되는 날, 기존 Plan은 다시 전개하지 않는다

섹션 제목: “상품 v2가 발행되는 날, 기존 Plan은 다시 전개하지 않는다”

상품팀이 v2를 발행하면 두 갈래가 생깁니다.

대상기본 적용이유
아직 구매하지 않은 고객새 구매 이벤트가 카탈로그 v2를 가리킴v2는 새로운 상업적 계약이므로 신규 Plan이 v2를 구매 기준 정보로 저장
이미 v1을 구매한 환자 A기존 Plan과 리비전은 v1 유지구매 당시의 권리와 예약 의무를 현재 카탈로그로 재해석하지 않음
기존 구매를 바꿔야 하는 예외명시적인 전환표·동의 후 새 리비전기존 계약 변경과 신규 계약을 같은 흐름으로 처리하지 않음

아래 시간축은 이 분기를 한눈에 보여 줍니다. 왼쪽의 v1 구매는 완료·확정 사실을 그대로 보존하고, v2 발행은 신규 구매와 기존 구매 전환 제안을 분리합니다. 기존 구매의 미래 항목도 자동으로 이동하지 않고, 승인과 동의가 있을 때만 같은 Plan 아래 새 리비전으로 넘어갑니다.

상품 카탈로그 v1이 환자 A의 구매 기준 정보와 v1 Plan으로 고정되고 완료·확정 사실은 보호되며, 카탈로그 v2는 신규 구매와 명시적 전환 승인·동의·새 Plan 리비전·CRM·상담 전달로 갈라지는 시간축 다이어그램
상품 v2 발행은 기존 Plan을 자동 재전개하지 않습니다. 신규 구매는 v2로 시작하고, 기존 구매의 미래 항목만 명시적인 전환표와 동의 뒤 새 리비전으로 승계할 수 있습니다.

완료된 것과 아직 하지 않은 것을 분리한다

섹션 제목: “완료된 것과 아직 하지 않은 것을 분리한다”

상품 전환에서 가장 위험한 말은 “남은 상품을 새 상품으로 바꾼다”입니다. 남았다는 표현 안에는 이미 진행한 항목, 확정했지만 아직 방문하지 않은 항목, 아직 제안하지 않은 항목이 섞일 수 있습니다.

현재 AppointmentPlanModel은 PLANNED, SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, BLOCKED_REVIEW 같은 Plan 항목 상태를 구분합니다. 상품 버전 전환에 사용하는 최소 모델은 PENDING, COMPLETED, CANCELLED를 별도로 검증합니다. 상태 이름이 조금 다르더라도 업무 원칙은 같습니다. 과거에 확정된 사실과 아직 협의 가능한 미래 의무를 분리해야 합니다.

Plan·방문 상태v2 발행 뒤 기본 처리환자 A에게 의미하는 것
COMPLETED기존 리비전과 기존 버전의 출처 이력을 영구 보존이미 제공된 서비스를 나중 상품 정의로 다시 쓰지 않음
IN_PROGRESS진행 중인 임상 사실과 현재 실행을 보호상품 개편으로 진행 중인 행위를 되돌리지 않음
CONFIRMED 방문에 연결된 항목당시 예약·적용 정책 기록·자원 합의를 보호확정된 시간과 항목을 자동 취소하지 않음
PLANNED 또는 PENDING 미래 항목승인된 전환표와 동의가 있을 때만 새 리비전 대상아직 확정하지 않은 부분만 협의 가능한 변경으로 봄
CANCELLED취소·환불 사실을 그대로 유지상품 v2 발행으로 취소된 의무를 부활시키지 않음

ProductVersionMigrationPlanner는 완료 항목을 매핑에 포함하면 거부하고, 모든 PENDING 원본 항목이 정확히 한 번 설명됐는지 검증합니다. 새 대상 치료 항목 키(target treatment key)도 중복될 수 없습니다. 이 검증은 상품 이름이 비슷하다는 이유로 과거 항목을 추측해 바꾸지 않기 위한 장치입니다.

기존 구매에 v2를 적용하는 예외

섹션 제목: “기존 구매에 v2를 적용하는 예외”

상품팀이 환자 A의 기존 구매에도 v2를 적용해야 한다면 예약 서비스가 항목 대응을 추측하지 않습니다. 상품팀이 대상 버전과 매핑, 동의 증거를 포함한 전환 사실을 만들고 고객 동의를 확보해야 합니다.

현재 ProductVersionMigration은 다음 여섯 가지 대응 방식을 구분합니다.

매핑원본 → 대상(target)환자 A 사례에서의 뜻
KEEP1 → 1같은 미래 의무를 v2의 새 항목 키로 유지
REPLACE1 → 1아직 하지 않은 기존 항목을 다른 v2 항목으로 교체
SPLIT1 → 2개 이상하나의 미래 항목을 둘 이상의 실행 항목으로 나눔
MERGE2개 이상 → 1여러 미래 항목을 하나의 새 실행 항목으로 묶음
REMOVE1개 이상 → 0환자에게 남은 의무를 새 리비전에서 제거
ADD0 → 1개 이상기존 원본 없이 새 의무를 추가

예를 들어 A의 패키지에 아직 시작하지 않은 항목 두 개와 이미 완료한 항목 하나가 있다고 합시다. 상품팀이 미래 항목 하나를 v2의 두 항목으로 SPLIT하고, 다른 항목은 KEEP으로 남기고, 완료 항목은 전환표에서 제외해야 합니다. 완료 항목을 REMOVE로 넣는 것은 “새 리비전에 표시하지 않기”와 “과거 완료 사실을 지우기”를 혼동하는 잘못된 매핑입니다.

전환이 승인되면 ProductVersionMigrationHandler는 다음 순서로 처리합니다.

  1. 원본 구매와 현재 활성 리비전을 잠근다.
  2. 현재 리비전이 fromProductVersionId 상품 버전인지 확인한다.
  3. 동의 subject와 증거의 유효성을 확인한다.
  4. 매핑과 미래 상품 BOM을 검증한다.
  5. 완료 항목은 구 리비전에 남기고 같은 Plan 아래 새 불변(immutable) 리비전을 추가한다(append).
  6. 새 리비전을 활성화하고 ProductVersionMigrationApplied 객관적 사실을 아웃박스(outbox)에 기록한다.

매핑, 동의, fromProductVersionId 상품 버전이 맞지 않으면 활성 리비전을 바꾸지 않고 격리·거부 결과를 남깁니다. 예약 서비스는 상품팀 대신 “이 상품이 고객에게 공정한가”를 판단하지 않습니다. 대신 전환 사실이 현재 Plan과 구조적으로 맞는지 검증하고, 무엇이 바뀌었는지 추적 가능한 리비전으로 남깁니다.

상품 변경이 방문 일정까지 바뀌는 순간

섹션 제목: “상품 변경이 방문 일정까지 바뀌는 순간”

상품의 BOM이 달라졌다고 해서 확정 예약의 날짜·시간·의사·장비를 즉시 다시 계산할 수는 없습니다. 예약에는 상품 기준 정보뿐 아니라 병원 운영 정책, 수용량, 환자 동의, 자원 합의가 함께 들어가기 때문입니다.

ProductVersionMigrationHandler의 구현 주석도 상품 버전 전환 처리에서 방문(appointment), 예약(commitment), 자원 배정(allocation)을 갱신하지 않는다고 명시합니다. 일정이 정말 바뀌어야 한다면 다음 흐름을 별도로 시작해야 합니다.

  1. 새 상품 항목으로 가능한 후보 시간을 계산한다.
  2. 기존 CONFIRMED 예약과 새 제안을 함께 보여 준다.
  3. 환자가 동의하면 새 제안과 변경 근거를 저장하고, 확정 제안을 교체한다.
  4. 환자가 거부하거나 후보가 없으면 기존 확정 예약은 먼저 지우지 않는다.
  5. 거부·재조정 결과를 CRM/상담과 커머스가 판단할 수 있는 객관적 사실로 전달한다.

이때 VIP 우선 예약이나 반복적인 노쇼 뒤 다음 예약에 추가 확인·제한을 두는 정책을 생각할 수 있습니다. 다만 이런 규칙을 예약 서비스 안에 임의로 숨겨서는 안 됩니다. 병원 운영 정책은 예약 서비스의 버전형 예약 정책으로 관리하고, 고객의 상담·연락·보상 판단은 CRM과 커머스가 소유합니다. 예약 서비스는 이번 제안에 적용한 정책과 계산 결과를 보존합니다. 내부 기준값과 고객 등급을 공개 데이터처럼 설명하지 않는 이유도 같습니다.

서비스소유하는 데이터 의미상품 변경 때 발행·소비하는 사실예약 서비스의 책임
상품 관리·상품 개발상품 버전, BOM, 전환표ProductCatalogChanged, ProductVersionMigrationApproved버전·BOM·매핑이 재현 가능한지 검증
구매·커머스구매 계약, 추가 구매, 환불PurchaseCompleted, PurchaseRefunded구매별 Plan 생성과 원본 구매 연결
예약 서비스버전형 예약 정책, Plan, Plan 리비전, 예약, 자원, 상태 이력ProductVersionMigrationApplied, 객관적 일정 사실기준 정보·리비전·확정 예약의 기준 보존
임상·시술시작·완료·부분 완료이행 사실완료 항목과 미래 항목 상태를 분리
고객 상담·CRM상담, 연락, 보상, 고객 동의 원본변경 거부·운영 전달객관적 사실을 받아 고객 판단을 수행
알림연락처 동의, 발송 이력예약·전환 이벤트 소비예약 트랜잭션과 채널 장애를 직접 결합하지 않음
통계·외부 소비자프로젝션과 지표Plan/리비전/이벤트 소비예약 원본을 대신하지 않음

책임 경계는 “이 데이터를 누가 읽을 수 있는가”보다 “누가 그 의미를 바꿀 수 있는가”에 가깝습니다. 상품 관리부는 v2를 발행할 수 있지만 A의 확정 예약을 직접 취소하지 않습니다. CRM은 동의 거부 뒤의 상담과 보상을 판단할 수 있지만 Plan 리비전의 출처 이력을 덮어쓰지 않습니다. 예약 서비스는 그 사이에서 실행 가능한 예약과 객관적 이력을 보존합니다.

중복 전달과 추가 구매는 다른 문제다

섹션 제목: “중복 전달과 추가 구매는 다른 문제다”

이 글에서 중복이라는 말을 “중복 구매”로 쓰지 않습니다.

중복 전달은 한 번의 구매에 대한 같은 PurchaseCompleted 이벤트가 타임아웃, 재시도, 재생으로 다시 도착하는 경우입니다. PurchaseCompletedHandlersourcePurchaseAuthority, sourcePurchaseId, tenant, clinic 범위의 기존 Plan을 먼저 확인합니다. 같은 구매와 불변 소유 정보가 맞으면 PURCHASE_ALREADY_PLANNED로 수렴하고 Plan을 새로 만들지 않습니다. payload가 다르면 두 번째 구매로 추측하지 않고 소유권 충돌(ownership conflict)로 격리합니다.

추가 구매는 환자가 실제로 다시 결제해 새로운 sourcePurchaseId를 받은 경우입니다. 이때는 새 구매가 새 Plan을 만듭니다. 두 Plan의 항목을 한 방문에 함께 배치하려면 별도의 자격 검증, 제안, 자원, 동의 검토가 필요합니다.

짧게 쓰면 다음과 같습니다.

같은 sourcePurchaseId + 같은 불변 소유 정보
└─> 기존 Plan으로 수렴
새 sourcePurchaseId
└─> 새로운 구매 Plan
중복 전달 ≠ 추가 구매

첫 번째는 같은 이벤트가 여러 번 도착해도 결과를 바꾸지 않는 처리 문제이고, 두 번째는 상품·커머스 업무입니다. 둘을 한 단어로 묶으면 상담 화면도 예약 화면도 환자에게 서로 다른 의미를 설명하게 됩니다.

현재 구현과 다음 설계를 구분해 읽기

섹션 제목: “현재 구현과 다음 설계를 구분해 읽기”
사실성 표지이 글에서 말하는 범위근거
현재 구현버전 카탈로그 프로젝션, 구매별 Plan 생성, 반복·BOM·의존성 기준 정보, 명시적 상품 버전 전환 매핑 검증, 같은 Plan의 리비전 추가·활성화, 완료 항목의 출처 이력 보존clinic-appointment develop 브랜치의 현재 원본과 관련 테스트
승인된 설계상품 변경으로 기존 확정 예약의 일정이 바뀌는 경우의 제안·동의·운영 정책, 복수 항목을 하나의 예약으로 묶는 확장예약 설계 문서, Plan·수용량 설계 문서
운영 대기실제 브로커 전달, 아웃박스(outbox) 카나리, 백필, 운영 준비 상태와 복구 훈련운영 적용·운영 절차(runbook)와 검증 결과가 필요한 범위
로드맵환자 포털에서 변경 제안을 확인하고 동의하는 공개 채널후속 제품·채널 범위

현재 구현이라고 쓴 항목도 “소스와 테스트에서 계약을 확인했다”는 뜻으로 제한합니다. 실제 병원에서 모든 상품 변경이 자동 적용되거나, 고객의 환불·보상 정책까지 예약 서비스가 수행한다고 읽으면 안 됩니다.

이번 글은 상품 버전이 바뀌어도 기존 Plan과 확정 예약을 다시 쓰지 않는 경계를 고정했습니다. 다음 글에서는 이벤트 상품의 최초 예약 규칙을 따라가며, 구매 직후 자동으로 날짜를 확정하지 않고 고객 희망 일정·유효 기간· 병원 자원을 어떻게 후보와 예약으로 연결하는지 살펴보겠습니다.

기존 보조 자료는 원본 설계 Markdown을 대체하지 않는 보조 자료입니다. 상품 예약 특성, 패키지 구성, 상품 BOM 전개를 먼저 비교하고 이 글의 상품 버전 전환 시간축으로 돌아오면 좋습니다.

댓글

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