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 | 예약상세 응답 → Booking | supplierIdentificationKey = sequence, pnr 없으면 UNDEFINED_PNR, voidable=true 고정 |
support/model/Booking.kt | 예약 도메인 | supplierIdentificationKey로 이후 모든 후속 호출(조회/취소/발권) 식별 |
configuration/RedisConfiguration.kt | Redis 직렬화 | GzipRedisSerializer + Jackson, FareItinerary 전용 Hash 템플릿 |
4. 공개 인터페이스 (컨트롤러 → application)
중앙 디스패처 없음
이 시스템은 중앙 라우터가 없습니다. 공급사 컨트롤러 자체가 Triple 예약 시스템의 내부 API로 노출됩니다(경로 prefix
/internals/GROUPAIR/...). 호출 관계 전체 지도는 caller-callee-map 참고.
4.1 컨트롤러 4종과 엔드포인트
| 컨트롤러 | @RequestMapping | 엔드포인트 (HTTP + 경로) | 위임 서비스 |
|---|---|---|---|
GroupairSearchController | /internals/GROUPAIR/search | POST / (검색), GET / (상세) | GroupairFlightSearchService, FlightAmenityService |
GroupairBookingController | /internals/GROUPAIR/bookings | POST / (예약), GET /{pnr}/confirm (확정조회), GET /{pnr} (조회), PUT /{pnr}/cancel (취소), GET /{pnr}/expected-cancel (예상환불), GET /{pnr}/cancelable (취소가능판정), PUT /{pnr} (APIS 변경) | GroupairBookingService, GroupairCancelService, GroupairPassengerService |
GroupairTicketingController | /internals/GROUPAIR/ticketing | POST / (발권) | GroupairTicketingService |
GroupairFareRuleController | /internals/GROUPAIR/fare-rules | GET / (운임규정), 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 분기 |
GroupairPassengerService | APIS(여권·체류정보) 변경 | 동기 단순 위임 (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) 순으로 무게를 올리는 것을 권장합니다.
다음으로 읽을 노트
- Group Air — 오퍼레이션 : 검색(goods)→예약(good 지정)→발권(2-스텝)→취소(VOID·REFUND) 시퀀스 디테일
- Group Air — 프로토콜 :
/goods/search경로 구성,agentId인증,GroupairResponse봉투, 채널/퍼널 엔드포인트 - Group Air — 함정 :
supplierIdentificationKeyvs PNR 식별,UNDEFINED_PNR, multiFare 미지원, 매진 운임 필터 - 시스템 아키텍처 · 요청 흐름 : 전체 그림