공통 오퍼레이션 생명주기

arch-overview pattern-strategy api-internals

이 노트의 목적

air-intl-adapter에는 11개 공급사 모듈이 있지만, 항공권을 팔기 위해 거치는 단계(오퍼레이션)는 공급사가 GDS든 NDC든 LCC든 사실상 동일하다. “검색 → 운임 재계산 → 예약 → 발권 → (취소/환불) → 사후처리”라는 비즈니스 흐름은 항공 산업 공통이기 때문이다. 이 시스템은 그 흐름을 interfaces/request·interfaces/response 패키지의 공통 DTO(계약) 로 못 박아 두고, 각 공급사 컨트롤러가 그 계약을 자기 방식으로 구현한다. 즉 “겉(계약)은 같고, 속(구현)만 다르다” — 이것이 어댑터 패턴의 핵심이다. 이 노트는 신입이 “한 번 흐름을 이해하면 11개 공급사 어디를 봐도 같은 지도를 쓸 수 있게” 만드는 것이 목표다.

시스템 전체 그림은 system-architecture, 한 요청이 레이어를 통과하는 과정은 request-flow, DTO 계약의 필드 상세는 interfaces-dtos를 함께 보라.


1. 한눈에 보는 생명주기

항공권 1장이 팔리고 (필요시) 취소되기까지의 전형적 흐름이다. 호출자는 Triple 예약 시스템이고, 각 단계는 별도 HTTP 호출이다 (중앙 디스패처 없음 — system-architecture 참고).

flowchart TD
    T["Triple 예약 시스템"]
    T -->|"공급사를 알고 URL 경로로 직접 호출"| EP["/internals/{SUPPLIER}/... 공급사별 자체 컨트롤러"]

    subgraph SELL["조회/판매 흐름"]
        SEARCH["SEARCH<br/>POST /search<br/>key 발급"]
        FARERULE["FARE-RULE<br/>GET /fare-rules<br/>운임 규정 텍스트"]
        REPRICING["REPRICING<br/>GET /bookings/{pnr}/repricing<br/>또는 SEARCH의 detail (운임 재확인)"]
        BOOKING["BOOKING (예약 생성)<br/>POST /bookings<br/>PNR 발급"]
        READY["TICKETING-READY (발권 직전 재조회)<br/>POST /ticketing/ready"]
        TICKETING["TICKETING (발권 = 결제+발권)<br/>POST /ticketing<br/>티켓번호 발급"]
        CANCEL["CANCEL / REFUND (취소·환불)<br/>PUT /bookings/{pnr}/cancel<br/>+ cancelable / expected-cancel 사전조회"]
        SEARCH --> FARERULE
        FARERULE --> REPRICING
        SEARCH --> REPRICING
        REPRICING --> BOOKING
        BOOKING --> READY
        READY --> TICKETING
        TICKETING --> CANCEL
    end

    subgraph POST["사후 처리 흐름"]
        QUEUE["QUEUE<br/>예약 후 대기열 조회/제거<br/>GET PUT /queues"]
        CASH["CASH-RECEIPT<br/>현금영수증 발행/취소<br/>POST /cash-receipts/issue<br/>PUT /cash-receipts/cancel"]
        ANCILLARY["ANCILLARY (부가서비스)<br/>수하물/좌석 구매·해제<br/>GET POST DELETE /ancillaries"]
        AGENCY["AGENCY-CREDIT<br/>대리점 예치금 잔액 조회 (LCC)<br/>GET /agency-credit"]
    end

    EP --> SEARCH
    EP --> QUEUE

"key"와 "PNR"이 단계를 잇는 두 개의 끈

  • key : SEARCH가 발급하는 운임 식별자(예: {SUPPLIER}_{UUID}). FARE-RULE·detail·BOOKING이 이 key로 “그때 그 운임”을 다시 찾는다.
  • pnr (또는 supplierIdentificationKey) : BOOKING이 발급하는 예약 식별자. REPRICING·READY·TICKETING·CANCEL·QUEUE·CASH-RECEIPT가 이 pnr로 예약을 가리킨다. 신입이 단계 간 흐름을 추적할 때는 “이 단계의 입력 식별자가 key인가 pnr인가” 를 먼저 보면 어느 단계인지 바로 안다.

2. 각 단계의 의미 (신입 눈높이)

2-1. SEARCH (검색) — POST /internals/{SUPPLIER}/search

사용자가 “ICN→NRT, 6/10 출발, 성인 1명”을 입력하면 공급사에서 살 수 있는 운임 목록을 받아온다.

  • 요청 DTO: SearchRequest (interfaces/request/SearchRequest.kt:9) — BaseSearchRequest(BaseSearchRequest.kt:5) 인터페이스를 구현. 노선·여정은 originDestinationLocationInfos: List<OriginDestinationLocationInfo>, 승객은 preferences: List<SearchPreference>로 표현.
  • 응답 DTO: List<FareItineraryView> (interfaces/response/FlightSearchView.kt:87) — 운임여정 요약 목록.
  • 상세 조회: 목록에서 하나를 고르면 GET /search(detail) → FareItineraryDetailView(FlightSearchView.kt:689)로 좌석/수하물/스케줄 상세를 받는다.
  • 반환 식별자: FareItineraryView.key (FlightSearchView.kt:89, 형식 {SUPPLIER}_{UUID})와 itemKey({SUPPLIER}_{UUID}::{SHA3-256}).
// AmadeusSearchController.kt:25-26 — 검색 컨트롤러 진입
@CircuitBreaker(name = "amadeusSearch", fallbackMethod = "searchFallback")
@PostMapping
fun search(@RequestBody request: SearchRequest): ResponseEntity<List<FareItineraryView>> {

검색은 공급사마다 "검색 가능 여부"를 먼저 거른다

SearchRequest.isSearchable(supplier) (SearchRequest.kt:26-34)가 공급사별 제약을 코드로 박아 둔다.

  • TWAY/JINAIR: 편도·왕복(여정 ≤ 2)만, 항공사코드(TW/LJ) 포함 시
  • SINGAPOREAIR: 항공사코드 SQ 포함 시
  • GROUPAIR: 왕복(round-trip)일 때만 호출자가 “모든 공급사에 똑같이 뿌려도” 안 맞는 공급사는 스스로 빠진다. 이 분기를 모르면 “왜 T’way만 검색 결과가 없지?”에서 한참 헤맨다.

SEARCH만 서킷브레이커가 fallback으로 빈 목록을 반환한다

AmadeusSearchController.kt:73-79searchFallback은 서킷이 OPEN이면 emptyList()를 돌려준다. “한 공급사가 죽어도 검색 화면은 떠야 한다”는 설계. 예약/발권 같은 돈이 오가는 단계는 fallback으로 가짜 성공을 만들면 안 되므로 이 패턴을 쓰지 않는다. → resilience-and-events


2-2. FARE-RULE / REPRICING (운임 규정 / 운임 재계산)

이 둘은 “결제 전에 운임을 다시 한 번 확인”하는 단계로 자주 묶여서 다뤄지지만 목적이 다르다.

구분무엇입력컨트롤러 예응답
FARE-RULE운임 규정(취소수수료·변경규정 등) 텍스트key (검색 운임)AmadeusFareRuleController.kt:20 GET /fare-rulesList<FareRuleView> (FareRuleView.kt:6) + /structuredStructuredFareRuleView
REPRICING이미 만든 예약의 가격 재확인pnr (예약)AmadeusBookingController.kt:129 GET /bookings/{pnr}/repricingRepricingView (RepricingView.kt:13)
  • REPRICING의 의미: 예약(PNR)은 만들었지만 아직 발권 안 한 상태에서, 공급사에 “지금 이 예약 얼마야?”를 물어 최신 운임을 다시 받는다. 항공 운임은 시간/좌석에 따라 변하므로, 발권 직전 가격이 검색 시점과 다를 수 있다. → 정밀 분석은 마이그레이션 문서/request-flow 참고.
  • REPRICING은 단순 retrieve가 아니다(공급사마다 다름):
    • Amadeus/Sabre: 별도 bookingService.repricing(pnr) 호출 (AmadeusBookingController.kt:131, SabreBookingController.kt:131).
    • Tway: bookingService.confirmPrice(pnr) (TwayBookingController.kt:133).
    • Singaporeair/Jejuair/Jinair: retrieve(pnr)의 승객 정보를 그대로 RepricingView로 변환 (SingaporeairBookingController.kt:113, JejuairBookingController.kt:100, JinairBookingController.kt:114).
  • REISSUE-SEARCH(재발행 검색): 발권된 티켓을 변경(재발행)하기 위한 검색. POST /search/reissue + ReissueSearchRequest(ReissueSearchRequest.kt:5, BaseSearchRequest 구현). amadeusndc·jejuair·jinair·koreanair·lufthansa·singaporeair·tway가 지원(아래 매트릭스).

REPRICING ≠ FARE-RULE ≠ REISSUE-SEARCH — 이름이 비슷해 헷갈린다

  • FARE-RULE: “이 운임의 규정 문구” (텍스트)
  • REPRICING: “내 예약의 현재 가격” (PNR 기준 금액)
  • REISSUE-SEARCH: “발권 후 변경하려는데 새 운임” (재발행용 검색) 세 단어가 모두 “운임”을 다뤄서 신입이 자주 섞는다. 입력이 key면 FARE-RULE/REISSUE-SEARCH, pnr이면 REPRICING이라는 점을 기억하라.

2-3. BOOKING (예약 생성) — POST /internals/{SUPPLIER}/bookings

검색한 운임(key)에 승객 정보를 붙여 PNR(예약번호)을 만든다. 아직 결제·발권은 안 한 상태.

  • 요청 DTO: BookingRequest (BookingRequest.kt:8) — key(검색 운임) + passengers: List<PassengerRequest> + 예약자 연락처.
  • 응답 DTO: BookingView (BookingView.kt:101) — pnr: PnrView(BookingView.kt:279), 스케줄, 승객, paymentTimeLimit(발권 마감시한) 등.
  • 컨트롤러 진입: AmadeusBookingController.kt:25-38 create().

/bookings 컨트롤러는 예약 생성 외에 예약 라이프사이클 전반을 담는다:

엔드포인트메서드의미
POST /bookingscreate예약 생성(PNR 발급)
GET /bookings/{pnr}retrieve예약 조회
GET /bookings/{pnr}/confirmconfirm예약 확정/재조회
GET /bookings/{pnr}/check-pnrcheckPnrPNR 유효성 확인 (Amadeus/Sabre/Singaporeair/Jejuair)
GET /bookings/{pnr}/repricingrepricing운임 재계산(위 2-2)
PUT /bookings/{pnr}changeApis승객 여권/APIS 정보 변경
POST /bookings/{pnr}/dividedividePNR 분리(일부 승객만 떼어냄)
PUT /bookings/{pnr}/cancelcancel취소(아래 2-5)

PNR이 곧 식별자, 단 NDC는 supplierIdentificationKey를 함께 쓴다

GDS(Amadeus/Sabre)는 6자리 PNR이 전부지만, NDC 계열(KoreanAir 등)은 예약 식별을 supplierIdentificationKey(Order ID)로 한다. KoreanairBookingController.kt:69,103처럼 @RequestParam supplierIdentificationKey필수로 받는 게 그 증거. PnrView(BookingView.kt:279-356)에 supplierIdentificationKey 필드가 있는 이유다. 같은 “예약 조회”라도 Amadeus는 GET /bookings/{pnr}만으로 되지만 KoreanAir는 쿼리 파라미터가 더 필요하다.


2-4. TICKETING (발권) — POST /internals/{SUPPLIER}/ticketing

PNR을 실제 항공권(티켓번호)으로 만든다. 돈이 오가는 단계. 보통 2-step이다.

flowchart TD
    READY["① READY<br/>POST /ticketing/ready<br/>발권 직전 마지막 재조회 (승객·스케줄 최신화)"]
    ISSUE["② ISSUE<br/>POST /ticketing<br/>결제 + 발권 → TicketingView (티켓번호)"]
    READY --> ISSUE
  • READY 요청/응답: TicketingRequest(TicketingRequest.kt:9) → TicketingReadyView(TicketingReadyView.kt:18, 승객·스케줄). 예: AmadeusTicketingController.kt:19-29.
  • ISSUE 요청/응답: TicketingRequestTicketingView(TicketingView.kt:50, 발권된 승객별 TicketView+PaymentView). 예: AmadeusTicketingController.kt:31-47.
  • 결제 정보: TicketingRequest.paymentInfo는 sealed class PaymentInfoRequest(TicketingRequest.kt:43)로 KEY_IN(카드 직접입력)·TOSS_PAY 두 갈래. prepayment=true면 paymentInfo 없이도 허용(선결제) — TicketingRequest.kt:19-23 init 블록이 이 불변식을 강제.

발권은 멱등하지 않다 — 두 번 부르면 중복 발권/중복 결제

READY와 ISSUE를 나눈 이유 중 하나가 “발권 직전 검증”이지만, ISSUE 자체는 재시도하면 위험하다. 그래서 발권 단계는 Resilience4j retry를 함부로 걸면 안 되는 대표 영역이다. 재발행(reissue)은 아예 비동기 폴링으로 뺀다(아래).

재발행(REISSUE/addition)은 코루틴+Redis 폴링 비동기다

발권 변경(재발행)은 시간이 오래 걸려 동기 응답이 어렵다. LCC들은 POST /ticketing/addition이 즉시 202 ACCEPTED + DeferredKeyView(폴링키)를 주고, 호출자가 GET /ticketing/addition/{reissueKey}DeferredView(DeferredView.kt:5: Pending/Complete)를 폴링한다.

  • 구현: TwayTicketingController.kt:54-92, JinairTicketingController.kt:53-93, JejuairTicketingController.kt:38-75 — 공통적으로 polling(...) / poller<ReissueResult<...>>(...) 유틸 + RedisTemplate 사용.
  • 비동기 메커니즘 자체는 async-coroutines, 예외 전파는 error-handling 참고. DeferredStatus.ERROR면 폴러가 저장된 throwable을 다시 던진다(TwayTicketingController.kt:79).

2-5. CANCEL / REFUND (취소·환불)

발권 전이면 “예약 취소”, 발권 후면 “발권 취소(VOID) 또는 환불(REFUND)“로 갈린다. 취소는 항상 사전조회 → 실행 2-step을 권장.

flowchart TD
    PRE["cancelable / expected-cancel<br/>취소하면 얼마 돌려받고 수수료 얼마인지 미리 본다"]
    CANCEL["cancel<br/>실제 취소 실행 → CancelView (voided + refunds)"]
    PRE --> CANCEL
엔드포인트메서드응답 DTO의미
GET /bookings/{pnr}/cancelablecancelableCancelableTypeDetailView취소 가능 형태(VOID vs REFUND) + 예상 환불
GET /bookings/{pnr}/expected-cancelexpectedCancelExpectedCancelView (ExpectedCancelView.kt:9)예상 취소 결과(채널별 규칙 적용)
PUT /bookings/{pnr}/cancelcancelCancelView (CancelView.kt:8)실제 취소. voided(무료취소 여부) + refunds: List<RefundView>
  • 요청 DTO: CancelRequest(CancelRequest.kt:10) — 결제정보(환불 처리용)·autoRefundable·waivers(면제 사유). validate()(CancelRequest.kt:20)가 결제정보 조합(KeyInCard / 전부 null / TossPay)이 유효한지 검사.
  • VOID vs REFUND: CancelView.voided(CancelView.kt:9)가 핵심. 발권 당일 취소면 VOID(무료, 환불내역 없음), 그 외는 REFUND(수수료 차감 후 환불). refundsrefundFee>0 || usedTax>0인 승객만 필터링해 담는다(CancelView.kt:18-20).

공급사마다 cancel의 "성공" 판정이 다르다

Amadeus: CancelView.of(cancelDetail)cancelDetail.action == VOID 판정(CancelView.kt:34-42). Sabre: 서비스가 환불 목록을 반환하고 refunds.isEmpty()이면 voided=true로 역산(SabreBookingController.kt:76-81). Singaporeair: 컨트롤러가 직접 (voided, refunds) 구조분해(SingaporeairBookingController.kt:43). 같은 CancelView를 응답하지만 만드는 방식이 제각각이다 — 한 공급사 코드를 다른 공급사에 복붙하면 안 되는 대표 사례.


2-6. QUEUE (대기열) — GET/PUT /internals/{SUPPLIER}/queues

GDS의 “큐(Queue)“는 항공사·발권센터가 후속 작업을 쌓아두는 작업함이다. 어댑터는 큐에 쌓인 PNR을 조회하고, 처리 끝난 PNR을 큐에서 제거한다.

  • 조회: GET /queuesList<QueueView>(QueueView.kt:8). 예: AmadeusQueueController.kt:16-21.
  • 제거: PUT /queues + List<QueueRemoveRequest>(QueueRemoveRequest.kt:3) → QueueRemoveView. AmadeusQueueController.kt:24-37(queueNumber, category, timeMode)로 묶어 일괄 제거.
  • 지원: amadeus·amadeusndc·galileo·sabre (GDS/NDC-GDS 계열만). LCC·NDC 직판에는 큐 개념이 없다.

2-7. CASH-RECEIPT (현금영수증) — /internals/{SUPPLIER}/cash-receipts

한국 세법상 현금결제 시 발행하는 현금영수증. 발행·취소를 어댑터가 대행한다.

  • 발행: POST /cash-receipts/issue + CashReceiptRequest(CashReceiptRequest.kt:6) → CashReceiptView(CashReceiptView.kt:8, 승인번호·승인시각·금액·티켓번호목록).
  • 취소: PUT /cash-receipts/cancel + CashReceiptCancelRequest(CashReceiptRequest.kt:14). 예: AmadeusCashReceiptController.kt.
  • 지원: amadeus·galileo·sabre.

2-8. ANCILLARY / AGENCY-CREDIT (부가서비스 / 대리점 크레딧) — LCC 중심

LCC(저비용항공)는 수하물·좌석을 별도 판매(ancillary)하고, 대리점 예치금(agency credit)으로 정산하는 경우가 많다.

  • ANCILLARY: /internals/{SUPPLIER}/ancillaries(jinair는 /ancillary 단수, 주의).
    • 가용 조회(GET /ancillaries/avail/key|pnr), 수하물 조회/구매(GET /baggage/..., POST /baggage), 해제(DELETE /ancillaries), 딥링크(POST /.../deep-link).
    • 요청 DTO: BaggageRequest/AncillaryReleaseRequest/AncillaryDeepLinkRequest/AncillaryPurchaseRequest(AncillaryRequest.kt). 예: TwayAncillaryController.kt.
    • 지원: tway·jinair·lufthansa·singaporeair (NDC도 부가서비스가 있어 lufthansa/singaporeair 포함).
  • AGENCY-CREDIT: GET /internals/{SUPPLIER}/agency-creditAgencyCreditView(amount)(AgencyCreditView.kt:3). 대리점 잔액 조회. 지원: tway·jinair. 예: TwayAgencyCreditController.kt.

3. 공통 DTO가 정의하는 “계약” — 어댑터의 심장

핵심 통찰: 계약(DTO)은 1벌, 구현( .of())은 11벌

interfaces/request·interfaces/response의 DTO는 공급사 중립적이다. Triple은 이 1벌의 계약만 알면 된다. 공급사 차이는 전부 응답 DTO의 companion object { fun of(...) } 팩토리 오버로딩으로 흡수된다.

가장 극적인 예가 FlightSearchView.kt다. 한 파일 상단(FlightSearchView.kt:10-86)에 11개 공급사의 domain.model.FareItinerary를 각각 alias import하고, FareItineraryView.of(...)를 공급사 타입마다 오버로드한다. FlightSearchView.of(fareItinerary: AmadeusFareItinerary)(:104), of(fareItinerary: GalileoFareItinerary)(:230), of(combinedFareItinerary: Pair<TwayFareItinerary, ...>)(:142) … 입력 타입만 다르고 출력은 모두 같은 FareItineraryView다.

// FlightSearchView.kt:142-167 — Tway는 출발/귀국편을 Pair로 결합해 하나의 View로 만든다(LCC 특성)
fun of(
    combinedFareItinerary: Pair<TwayFareItinerary, TwayFareItinerary?>,
    tripDirectionType: TripDirectionType? = null,
): FareItineraryView { ... }   // ← GDS는 단일 itinerary, LCC는 편별 결합. 같은 View, 다른 조립.

단계별 공통 DTO 빠른 표

단계요청 DTO응답 DTO정의 파일
SearchSearchRequest / SearchDetailRequestFareItineraryView / FareItineraryDetailViewSearchRequest.kt, FlightSearchView.kt
Reissue-SearchReissueSearchRequestFareItineraryViewReissueSearchRequest.kt, FlightSearchView.kt
Fare-Rule(쿼리: key, adult/child/infant)FareRuleView / StructuredFareRuleViewFareRuleView.kt, StructuredFareRuleView.kt
Repricing(path: pnr)RepricingViewRepricingView.kt
BookingBookingRequest / BookingChangeRequest / BookingDivideRequestBookingView / BookingChangeViewBookingRequest.kt, BookingView.kt
Ticketing-ReadyTicketingRequestTicketingReadyViewTicketingRequest.kt, TicketingReadyView.kt
Ticketing-IssueTicketingRequestTicketingViewTicketingRequest.kt, TicketingView.kt
Reissue(addition)ReticketingRequestDeferredKeyViewDeferredView<ReticketingView>TicketingRequest.kt, DeferredView.kt, ReticketingView.kt
CancelCancelRequestCancelView / CancelableTypeDetailView / ExpectedCancelViewCancelRequest.kt, CancelView.kt, ExpectedCancelView.kt
Refund(내역)RefundViewRefundView.kt
QueueQueueRemoveRequestQueueView / QueueRemoveViewQueueRemoveRequest.kt, QueueView.kt
Cash-ReceiptCashReceiptRequest / CashReceiptCancelRequestCashReceiptViewCashReceiptRequest.kt, CashReceiptView.kt
AncillaryBaggageRequest / AncillaryReleaseRequest / AncillaryDeepLinkRequest / AncillaryPurchaseRequestAncillaryViewAncillaryRequest.kt, AncillaryView.kt
Agency-CreditAgencyCreditViewAgencyCreditView.kt

필드 단위 상세(예: FareViewqCharge, TicketViewconjunctionTicketNumber)는 interfaces-dtos에서 다룬다.


4. 공급사 × 오퍼레이션 지원 매트릭스 (컨트롤러 실재 기준)

이 표는 추측이 아니라 interfaces/controller/internals/*Controller.kt의 실제 존재 여부로 작성됨

근거: 전체 컨트롤러 목록과 각 @RequestMapping 경로를 스캔(find ... *Controller.kt).

  • O = 해당 오퍼레이션 컨트롤러/엔드포인트 존재, ─ = 없음
공급사유형SearchReissue-SearchFareRuleRepricingBookingTicketingReissue(addition)Cancel/RefundQueueCashReceiptAncillaryAgencyCredit
amadeusGDS(1A)OOOOOOOO
sabreGDS(1S)OOOOOOOO
galileoGDS(1G)OOOOOOOO
amadeusndcNDCOOOOOOOO
koreanairNDCOOOOOO
lufthansaNDCOOOOOOOO
singaporeairNDCOOOOOOOO
twayLCCOOOOOOOOOO
jinairLCCOOOOOOOOOO
jejuairLCCOOOOOOOO
groupair그룹OOOOO(취소만)

표를 읽는 법 / 주의점

  • Search·FareRule·Booking·Ticketing·Cancel은 11개 전부 지원 = 이것이 “공통 핵심 오퍼레이션”이다. 신입은 이 5개만 먼저 이해해도 어느 공급사든 큰 그림을 잡는다.
  • Queue/CashReceipt = GDS 전유물. amadeus·sabre·galileo만 가짐(amadeusndc는 Queue만). LCC/NDC직판엔 큐·현금영수증 개념이 없다.
  • Reissue(addition, 비동기 발권변경) = LCC 전유물. tway·jinair·jejuair만 POST /ticketing/addition을 가짐. → async-coroutines
  • Ancillary/AgencyCredit = LCC 중심(단 Ancillary는 NDC인 lufthansa·singaporeair도 보유).
  • groupair는 가장 작은 모듈: repricing·divide 엔드포인트가 없고 cancel은 있지만 cancelable까지만(취소/조회 위주). 단체운임 특성상 개별 가격 재계산을 하지 않는다 — GroupairBookingController.kt에 repricing/divide 부재 확인.
  • koreanair에는 repricing 엔드포인트가 없다(GET /bookings/{pnr}/repricing 부재). NDC Order 모델에서 재계산을 다르게 처리.

5. 신입이 자주 빠지는 함정 (요약)

핵심 함정 모음

  1. “같은 URL = 같은 동작”이 아니다. GET /bookings/{pnr}도 Amadeus는 pnr만, KoreanAir는 supplierIdentificationKey 쿼리 필수.
  2. repricing이 retrieve인 공급사와 별도 호출인 공급사가 섞여 있다(2-2 표). 응답이 같아 보여도 비용·부작용이 다르다.
  3. 발권(ISSUE)·재발행은 멱등하지 않다. 재시도/리트라이를 무심코 걸면 중복 결제. → resilience-and-events
  4. 재발행은 비동기 폴링이라 즉시 응답(202)을 “실패”로 오해하기 쉽다. DeferredView.Pending이 정상 상태다.
  5. 공급사별 “검색 가능 조건”(isSearchable, SearchRequest.kt:26)을 모르면 “결과 없음”의 원인을 못 찾는다.
  6. /ancillaries(복수) vs jinair /ancillary(단수) 경로 표기가 다르다.

세부 지뢰는 landmines, 디버깅 연습은 exercises-debugging, 단계별 더 깊은 코드 추적은 각 공급사의 *-operations 노트로 이어진다.


6. 더 깊이 — 대표 공급사 오퍼레이션 노트로

학습 순서 추천

① 이 노트로 흐름 골격 → ② interfaces-dtos로 계약 필드 → ③ request-flow로 한 요청의 레이어 통과 → ④ 관심 공급사 *-operations로 구현 디테일. 온보딩 전체 지도는 onboarding-map, 빠른 참조는 quick-reference.