Group Air — 개요

module-groupair arch-supplier-module api-rest pattern-overview

이 노트의 위치

이 문서는 air-intl-adapter의 11개 공급사 모듈 중 Group Air(그룹/단체 운임 공급사) 모듈의 진입점입니다. 11개 모듈 중 가장 작습니다(34파일 / 1,921줄). 전체 아키텍처는 system-architecture, 요청 한 건이 흐르는 경로는 request-flow를 먼저 읽고 오면 이해가 빠릅니다. 오퍼레이션 디테일은 groupair-operations, 프로토콜/엔드포인트는 groupair-protocol, 함정은 groupair-pitfalls로 이어집니다.


1. 공급사 특징 — 왜 이렇게 연동하는가

1.1 “항공사”가 아니라 그룹/단체 운임 콘솔리데이터(중개사) REST API

Group Air는 특정 항공사가 아닙니다. amadeus·sabre·galileo가 GDS(SOAP), koreanair·lufthansa·singaporeair가 NDC, tway·jinair·jejuair가 항공사 직판 LCC REST인 반면, Group Air는 그룹/단체 운임을 모아 파는 외부 콘솔리데이터(consolidator)의 사설 JSON REST API에 직결합니다.

코드로 확인되는 근거:

근거위치의미
단일 캐리어가 아니라 validatingCarrier를 검색 결과마다 받음infrastructure/response/GoodSearchItemResponse.kt:18항공사 고정이 아닌 여러 항공사의 그룹 좌석을 중개
검색 경로가 항공편이 아니라 **/goods/search/** (“상품” 검색)infrastructure/GroupairClient.kt:55항공권을 “상품(goods)“으로 취급 — 콘솔리데이터 도메인 용어
GoodCategory { REAL_TIME, PACKAGE, FREE }support/enums/GoodCategory.kt실시간/패키지(단체)/무료 상품 구분
인증이 토큰/세션이 아니라 agentId(대리점 ID) 쿼리 파라미터GroupairClient.kt:58,111,149 / Properties.kt:474대리점(agency) 단위 B2B 연동 — GDS PNR 세션도, LCC 토큰도 없음
Supplier.GROUPAIR 고정 + funnel/channel별 엔드포인트domain/model/FareItinerary.kt:26 / Properties.kt:477-505단일 공급사 모듈, 채널/퍼널별 다른 콘솔리데이터 백엔드
GroupairResponse<T>{ code, data, message } 제네릭 봉투, code != "OK"면 에러infrastructure/response/GroupairResponse.kt순수 JSON 응답 공통 래퍼 (SOAP/XML 아님)

"그룹/단체 운임"의 실제 의미

단체 운임은 항공사가 여행사·콘솔리데이터에 블록(block)으로 좌석을 도매로 풀고, 콘솔리데이터가 이를 소매로 재판매하는 구조입니다. 그래서 이 모듈의 도메인 단위는 “항공편”이 아니라 “good(상품)” 이고, 예약 시 departureGoodSequence(출발 상품 일련번호)로 상품을 지정합니다(infrastructure/request/ReservationCreateRequest.kt:14). 일반 GDS/NDC 예약처럼 운임을 실시간 재계산(repricing)하지 않고, 콘솔리데이터가 미리 만들어 둔 상품을 그대로 잡습니다.

1.2 비즈니스/시장 맥락 — 왜 별도 모듈로 두는가

  • 단체/그룹 운임은 일반 GDS·NDC 채널에서는 노출되지 않는 별도 인벤토리입니다. 콘솔리데이터만 접근 가능한 도매 재고를 Triple에 추가 노출하기 위해 별도 어댑터가 필요합니다.
  • 항공사별 직연동(NDC/LCC) 11개를 다 붙이는 대신, 여러 항공사 그룹 좌석을 한 번에 중개하는 콘솔리데이터 1곳에 붙이면 노선 커버리지를 넓게 확보할 수 있습니다(검색 결과의 validatingCarrier가 매번 다른 이유).
  • 인증이 단순(대리점 ID)하고 세션 상태가 없어 모듈이 얇습니다. 복잡도는 콘솔리데이터 백엔드가 흡수하고, 어댑터는 얇은 변환(adapter) 계층만 담당합니다.

1.3 비동기는 거의 없음 — “조회 후 행동” 동기 모델

amadeus/sabre의 코루틴·galileo의 SessionContext 같은 복잡한 비동기 상태기계가 없습니다. 코루틴 사용처는 단 두 군데, 모두 부수적입니다.

비동기 사용처위치목적
검색 병렬화 withBlocking(Dispatchers.IO) { ... .pmap { ... } }application/GroupairFlightSearchService.kt:45-64출발-도착 location 쌍을 카테시안 곱으로 펼쳐 병렬 검색
fire-and-forget 검색키 제거 CoroutineScope(Dispatchers.IO).withLaunch { ... }application/GroupairBookingService.kt:39-43예약 성공 후 검색 캐시키 비동기 정리

withBlocking/withLaunch/pmap는 공통 유틸 support/util/CoroutineExtensions.kt 소속입니다(async-coroutines 참고). 그 외 취소·발권·운임규정은 전부 동기 호출입니다.


2. 모듈 규모와 서브패키지 구조

34개 .kt 파일 / 약 1,921 라인으로, 11개 공급사 중 압도적으로 가장 작습니다. 비교: jejuair 99파일/5,131줄, amadeus 873파일, sabre 882파일, galileo 525파일. 단체 운임이라 부가서비스(Ancillary)·대리점 크레딧(AgencyCredit)·재발행(reissue) 같은 무거운 오퍼레이션이 없고, 인증 상태기계도 없기 때문입니다.

supplier/groupair/                         # 34 files / ~1,921 LOC (최소 모듈)
├── application/          # 6개 서비스 (오케스트레이션 — 대부분 얇은 위임)
│   ├── GroupairFlightSearchService.kt   # 검색 + 캐시 + 카테시안 병렬
│   ├── GroupairBookingService.kt        # 예약 생성/조회 + 검색키 비동기 정리
│   ├── GroupairTicketingService.kt      # 예약확정(update) → 발권(channel-confirm)
│   ├── GroupairCancelService.kt         # 취소 가능 판정 / 예상환불 / 취소
│   ├── GroupairPassengerService.kt      # APIS(여권/체류정보) 변경 위임
│   └── GroupairFareRuleService.kt       # 운임규정 조회(Redis 캐시)
├── infrastructure/       # 외부 콘솔리데이터 REST 호출
│   ├── GroupairClient.kt                # 단일 클라이언트, 8개 엔드포인트
│   ├── request/   (1 파일, 4 DTO)       # ReservationCreate/Update, PassengerCreate/ApisUpdate
│   └── response/  (7 파일)              # GroupairResponse(봉투), GoodSearchItem, ReservationDetail ...
├── domain/
│   ├── model/GroupairFlightSearch.kt    # FareItinerary/PassengerFare/Schedule/Segment 도메인
│   └── repository/GroupairFareItineraryRepository.kt  # Redis Hash 저장소
├── interfaces/
│   └── controller/internals/            # 4개 REST 컨트롤러 (Triple 내부 API)
│       ├── GroupairSearchController.kt
│       ├── GroupairBookingController.kt
│       ├── GroupairTicketingController.kt
│       └── GroupairFareRuleController.kt
├── support/
│   ├── Constants.kt                     # UNDEFINED_PNR = "UNKNOWN"
│   ├── enums/    (5개)                  # GoodCategory, FareDiscountType, PnrStatus, SeatGrade, PassengerTypeCode
│   ├── model/    (6개)                  # Booking, Fare, Passenger, Schedule, Ticket, CancelableTypeDetail
│   └── util/AirportUtils.kt             # 출발/도착 location → OriginDestination 펼치기
└── configuration/
    └── RedisConfiguration.kt            # FareItinerary 전용 Gzip Redis 템플릿 (GroupairRedisConfiguration)

GroupairProperties는 모듈 밖

채널/퍼널별 콘솔리데이터 엔드포인트(searchEndpoint, bookingEndpoint)와 agencyId를 담는 GroupairProperties/GroupairApiProperties는 공통 설정에 있습니다(configuration/Properties.kt:470-505). 모듈 내 configuration/에는 Redis 템플릿(GroupairRedisConfiguration)만 존재합니다. 설정 전반은 configuration-and-infra 참고.


3. 핵심 파일 표

파일역할핵심 포인트
infrastructure/GroupairClient.kt (337줄)유일한 외부 통신 진입점8개 엔드포인트, agentId 쿼리 인증, GroupairResponse.checkError로 일괄 에러 처리, searchTimeout=30s / defaultTimeout=60s
application/GroupairFlightSearchService.kt검색 오케스트레이션pmap 병렬 검색, 캐시키↔운임 매핑, 매진(unexposed) 운임 필터, preferences.first()만 사용(multiFare 미지원)
application/GroupairBookingService.kt예약 생성/조회departureGoodSequence로 상품 예약, 성공 후 withLaunch로 검색키 비동기 제거
application/GroupairCancelService.kt취소 판정/예상환불/취소tickets 유무로 VOID vs REFUND 분기, 환불비 > 0인 승객만 노출
application/GroupairTicketingService.kt발권updateReservation(주문번호 기록) → ticketing(channel-confirm) 2-스텝
infrastructure/GroupairClient.kt → createReservation예약 요청 변환ReservationCreateRequest.of(...)FareItinerary+승객을 콘솔리데이터 포맷으로
domain/model/GroupairFlightSearch.kt검색결과 도메인 + 캐시 키id=SHA3(scheduleKey+fareKey), itemKey="$key::$id", validate()(child/infant 운임 존재 검증)
infrastructure/response/ReservationDetailResponse.kt예약상세 응답 → BookingsupplierIdentificationKey = sequence, pnr 없으면 UNDEFINED_PNR, voidable=true 고정
support/model/Booking.kt예약 도메인supplierIdentificationKey로 이후 모든 후속 호출(조회/취소/발권) 식별
configuration/RedisConfiguration.ktRedis 직렬화GzipRedisSerializer + Jackson, FareItinerary 전용 Hash 템플릿

4. 공개 인터페이스 (컨트롤러 → application)

중앙 디스패처 없음

이 시스템은 중앙 라우터가 없습니다. 공급사 컨트롤러 자체가 Triple 예약 시스템의 내부 API로 노출됩니다(경로 prefix /internals/GROUPAIR/...). 호출 관계 전체 지도는 caller-callee-map 참고.

4.1 컨트롤러 4종과 엔드포인트

컨트롤러@RequestMapping엔드포인트 (HTTP + 경로)위임 서비스
GroupairSearchController/internals/GROUPAIR/searchPOST / (검색), GET / (상세)GroupairFlightSearchService, FlightAmenityService
GroupairBookingController/internals/GROUPAIR/bookingsPOST / (예약), GET /{pnr}/confirm (확정조회), GET /{pnr} (조회), PUT /{pnr}/cancel (취소), GET /{pnr}/expected-cancel (예상환불), GET /{pnr}/cancelable (취소가능판정), PUT /{pnr} (APIS 변경)GroupairBookingService, GroupairCancelService, GroupairPassengerService
GroupairTicketingController/internals/GROUPAIR/ticketingPOST / (발권)GroupairTicketingService
GroupairFareRuleController/internals/GROUPAIR/fare-rulesGET / (운임규정), GET /structured (구조화)GroupairFareRuleService, GroupairFlightSearchService

서킷브레이커는 검색에만

GroupairSearchController.search()에만 @CircuitBreaker(name = "groupairSearch", fallbackMethod = "searchFallback")가 걸려 있습니다(GroupairSearchController.kt:25). 열리면 fallback이 빈 리스트를 반환하고 Datadog 스팬에 supplier.circuit-breaker=OPEN 태그를 남깁니다(:68-76). 예약/발권/취소엔 서킷브레이커가 없습니다. 서킷브레이커·리트라이의 큰 그림은 resilience-and-events 참고.

pnr 식별이 아니라 supplierIdentificationKey 식별

Booking 컨트롤러의 경로 변수는 {pnr}이지만, 실제 후속 호출의 키는 쿼리 파라미터 supplierIdentificationKey(콘솔리데이터의 예약 sequence)입니다(GroupairBookingController.kt:37,46,79,101). PNR은 없을 수도 있어(UNDEFINED_PNR = "UNKNOWN", support/Constants.kt) 조회 시 pnrNumber 쿼리는 PNR이 정의된 경우에만 붙습니다(GroupairClient.kt:174). 자세한 함정은 groupair-pitfalls.

4.2 application 서비스 6종

서비스책임비동기/복원성 표식
GroupairFlightSearchService검색 + 검색상세(getFareItinerary)withBlocking+pmap 병렬, 매진 운임 필터
GroupairBookingService예약 생성/조회withLaunch fire-and-forget(검색키 제거)
GroupairTicketingService발권(예약확정 → channel-confirm)동기 2-스텝
GroupairCancelService취소가능 판정 / 예상환불 / 취소동기, VOID·REFUND 분기
GroupairPassengerServiceAPIS(여권·체류정보) 변경동기 단순 위임 (1줄)
GroupairFareRuleService운임규정 조회(Redis 캐시)동기, 캐시 미스 시만 외부 호출

지원 오퍼레이션 = Search · Booking · Ticketing · FareRule (4종)

상위 컨텍스트의 공식 오퍼레이션 목록과 일치합니다. tway/jinair에 있는 Ancillary(부가서비스)·AgencyCredit(대리점 크레딧) 전용 컨트롤러는 groupair에 없습니다. 취소·APIS 변경은 별도 오퍼레이션이 아니라 Booking 컨트롤러 안의 엔드포인트로 포함됩니다(PUT /{pnr}/cancel, PUT /{pnr}). 단체 운임은 미리 묶인 상품을 잡는 구조라 좌석/수하물 추가판매·재발행이 불필요하기 때문입니다.


5. 중요도 별점

중요도: ★☆☆ (낮음 — 단, 학습 입문용으로는 ★★☆)

항목평가근거
코드 규모최소34파일 / 1,921줄 — 11개 모듈 중 11위(가장 작음)
비즈니스 중요도보통단체/그룹 운임이라 GDS·NDC에 없는 별도 인벤토리를 보강하지만, 전체 거래량 비중은 메이저 GDS/LCC보다 작음
학습 가치높음(입문)공급사 모듈의 “골격”을 가장 군더더기 없이 보여주는 표본. 컨트롤러 4종 → 서비스 6종 → 단일 클라이언트 → Redis 캐시로 이어지는 레이어드 구조와 @CircuitBreaker + GroupairResponse.checkError 에러 모델을 한눈에 파악 가능
위험도낮음~보통인증 상태기계·재발행·결제 직수행이 없어 함정이 적음. 다만 취소/환불을 직접 수행하고 supplierIdentificationKey 식별 혼동 여지가 있음 (groupair-pitfalls)

온보딩 추천 순서: air-intl-adapter에서 가장 먼저 읽기 좋은 공급사 모듈. 양이 적고 비동기/세션/암호화 부담이 없어 “컨트롤러→서비스→인프라→도메인” 4레이어 흐름과 검색→예약→발권→취소 라이프사이클을 단숨에 따라갈 수 있습니다. 골격을 여기서 익힌 뒤 jejuair(LCC REST) → koreanair(NDC) → amadeus(GDS) 순으로 무게를 올리는 것을 권장합니다.


다음으로 읽을 노트