Lufthansa — 개요

module-lufthansa arch-supplier-module api-ndc pattern-rest-controller

한 줄 요약

Lufthansa(LHG: LH·LX·OS)는 NDC V17.2(EDIST 스키마) 표준을 SOAP 봉투로 감싸 호출하는 FSC(Full Service Carrier) 모듈이다. 파일 수는 87개로 작아 보이지만, 그중 54개·6,726줄이 infrastructure(NDC 메시지 DTO) 에 몰려 있어 “프로토콜 무게”는 GDS 모듈 다음으로 무겁다. 재발행(Reissue)·환불계산(Reshop)·부가서비스(Ancillary)까지 실제로 구현된, 학습 가치가 높은 NDC 표본이다.

관련 노트: 오퍼레이션 상세 · SOAP) 상세 · 함정 모음 · 전체 아키텍처 · 요청 흐름


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

LCC / FSC / GDS 분류 — 코드로 검증

Lufthansa는 FSC(대형 항공사)이며, 항공사 직접 연동인 NDC(New Distribution Capability) 방식이다. GDS(Amadeus/Sabre/Galileo)도 LCC REST(Tway/Jinair/Jejuair)도 아니다. 근거:

증거위치의미
스키마 디렉터리 NDC_V17.2_Schema_V2023.3src/test/schema/lufthansa/IATA NDC V17.2 표준 사용
edist_commontypes.xsd, edist_structures.xsd위 디렉터리NDC의 메시지 포맷 EDIST(Enhanced Distribution) 스키마
AirShoppingRQ.version = "17.2"infrastructure/request/AirShoppingRQ.kt:11모든 RQ DTO가 버전 17.2 하드코딩
PADIS codeset element 9873 주석infrastructure/request/Preference.kt:40NDC 코드셋(PADIS) 직접 참조
검증 대상 항공사 LH, LX, OSLufthansaCertificationTest.kt:190LHG 그룹(Lufthansa·SWISS·Austrian)

NDC를 "SOAP으로" 부른다는 역설

순수 NDC는 보통 REST/JSON이지만, Lufthansa Partner API는 NDC EDIST XML 페이로드를 SOAP 봉투(<soap:Envelope>)에 넣어 전송한다. LufthansaClientsoapRequestBodyConverter(infrastructure/LufthansaClient.kt:866)가 NDC XML을 <ns1:XXTransaction> 바디 안에 childDocument("REQ", ...)로 끼워 넣고, SOAP 헤더에 <iden>(자격증명)과 FLXDM 디스패치 스크립트를 붙인다. 즉 표준은 NDC, 전송은 SOAP이라는 하이브리드. 이 때문에 응답 파싱도 soapBodyDeserializerOf(...)로 SOAP Body를 벗긴 뒤 NDC 엘리먼트를 역직렬화한다.

비즈니스·시장 맥락

  • 왜 NDC인가: FSC는 부가서비스(좌석/수하물)·운임 패밀리·재발행 같은 리치 콘텐츠를 GDS보다 풍부하게 NDC로 노출한다. Lufthansa는 NDC 선도 항공사로, 직판 채널에 NDC를 강제(GDS 서차지)해 왔다. Triple이 NDC를 직접 물린 이유가 여기에 있다.
  • certification 시나리오의 존재: src/test/schema/lufthansa/certification/truereshop, truereshop2 두 개의 재발행(reshop) 인증 시나리오가 8단계 RQ/RS XML(1_before_OrderRetrieveRQ → … → 8_after_OrderRetrieveRS)로 박제되어 있다. NDC 항공사는 연동 전 인증(certification) 통과가 필수이며, 이 폴더가 그 흔적이다. LufthansaCertificationTest.kt는 검색→가격→예약생성→발권→리트리브→환불계산→취소 전 과정을 실항공권으로 도는 통합 인증 테스트다.
  • 다중 캐리어 한 모듈: LH/LX/OS를 한 모듈이 담당한다. 검증 운임의 validatingCarrier로 캐리어를 구분하며, 일부 코드는 validatingCarrier를 보정한다(예: 부가서비스 구매 시 "LX" → "LXA" 치환, LufthansaAncillaryService.kt:156).

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

전체 87개 파일 / 약 8,968줄(메인 소스). 11개 공급사 중 파일 수로는 작은 편(groupair 34 < singaporeair 88 ≈ lufthansa 87 < koreanair 213 …)이나, 줄 수는 NDC 메시지 DTO 때문에 부풀어 있다.

서브패키지별 규모 (main, *.kt)
├─ infrastructure/   54 files  6,726 loc   ← 전체의 75%. NDC 메시지 DTO 집중
│   ├─ request/      20 files               (AirShoppingRQ, OfferPriceRQ, OrderCreateRQ,
│   │                                        OrderChangeRQ, OrderReshopRQ, OrderCancelRQ,
│   │                                        OrderRetrieveRQ, ServiceListRQ, SeatAvailabilityRQ ...)
│   ├─ response/     33 files               (AirShoppingRS, OfferPriceRS, OrderViewRS,
│   │                                        OrderReshopRS, OrderCancelRS, ServiceListRS ...)
│   └─ LufthansaClient.kt (908 loc)         ← 외부 API 호출 단일 진입점
├─ application/       7 files    795 loc    ← 서비스 7종 (서비스당 평균 ~110줄)
├─ support/          18 files    627 loc
│   ├─ model/        13 files               (Booking, Passenger, Fare, OfferPriceInfo, Schedule ...)
│   ├─ enums/         3 files               (CommissionType, LufthansaSoapHeaderNamespace, PassengerTypeCode)
│   └─ util/          2 files               (AirportUtils, FareItineraryScoring)
├─ interfaces/        5 files    502 loc    ← 컨트롤러 5종 (DTO는 공용 interfaces 패키지 재사용)
│   └─ controller/internals/
├─ domain/            2 files    289 loc
│   ├─ model/LufthansaFlightSearch.kt (253 loc, FareItinerary 도메인)
│   └─ repository/LufthansaFareItineraryRepository.kt
└─ configuration/     1 file      29 loc    (LufthansaRedisConfiguration)

신입을 위한 독해 순서

  1. 컨트롤러 5종으로 무슨 API가 노출되는지 본다 → 2) 같은 이름의 application 서비스로 유스케이스 흐름을 따라간다 → 3) LufthansaClient의 동명 메서드로 외부 호출과 에러 처리를 본다 → 4) 여유가 있으면 infrastructure/request|response의 DTO를 본다. DTO 53개를 처음부터 읽지 말 것. 흐름을 잡은 뒤 필요한 메시지만 펼쳐 보는 게 정석이다.

3. 핵심 파일 표

파일역할한 줄 핵심
infrastructure/LufthansaClient.kt (908줄)외부 API 클라이언트검색/가격/예약/발권/취소/재발행/부가서비스 모든 외부 호출의 단일 진입점. soapRequestBodyConverter가 NDC↔SOAP 변환을 담당
application/LufthansaFlightSearchService.kt검색 서비스다구간 cartesian product + pmap 병렬 검색, 점수화·정렬, Redis 캐시. NDC 재발행 검색(reissueSearch)도 포함
application/LufthansaBookingService.kt예약 서비스예약 전 OfferPrice(pricing) 선행 필수(book()), 분할(divide) 사전검증
application/LufthansaTicketingService.kt발권 서비스savePayment로 발권, 실패 시 코루틴 비동기 자동취소(cancelAsync), 재발행 폴링
application/LufthansaCancelService.kt취소 서비스OrderReshop로 환불액 선계산 후 VOID/REFUND 판정 → OrderCancel
application/LufthansaPassengerService.kt승객 APIS 변경변경분만 추려 OrderChange 호출. null이면 기존 값이 삭제되는 NDC 특성 보정
application/LufthansaAncillaryService.kt부가서비스좌석·추가수하물 검색/구매. pmap 병렬, validatingCarrier 보정
application/LufthansaFareRuleService.kt운임규정OfferPrice로 규정 조회, SOLD_OUT 시 미노출 운임 등록
infrastructure/request/OrderChangeRQ.kt만능 변경 RQofApis/ofPayment/ofDivide/ofReissue/ofBookAncillaries/ofPurchaseAncillaries8개 팩토리가 한 메시지를 재사용
infrastructure/request/OrderReshopRQ.kt재가격 RQofRefundCalculate(환불계산)·ofReissueSearch(재발행검색) 두 용도 공유
domain/model/LufthansaFlightSearch.kt (253줄)도메인 FareItineraryRedis 캐시 단위. 점수/스케줄/운임 보유
support/util/FareItineraryScoring.kt정렬 점수가격·비행시간 min/avg/max 기반 withScore
configuration/RedisConfiguration.ktRedis 빈lufthansaFareItineraryRedisTemplate + Gzip 직렬화(NDC 운임 객체가 크다)
support/enums/LufthansaSoapHeaderNamespace.ktSOAP 네임스페이스TRANSACTION 헤더 네임스페이스 정의
src/main/resources/supplier/lufthansa.yml환경 설정dev/qa/staging/prod별 AWS Secrets Manager에서 자격증명 로드
configuration/Properties.kt:346 (공용)LufthansaPropertieschannel/funnel 2단계로 자격증명 분기(getApiProperties)

4. 공개 인터페이스 (컨트롤러 → 엔드포인트 → 서비스)

모든 컨트롤러는 interfaces/controller/internals/ 아래의 @RestController이며, Triple 예약 시스템이 호출하는 내부 전용 API다. 중앙 디스패처는 없고 /internals/LUFTHANSA/... 경로가 곧 진입점이다(system-architecture 참고).

4.1 LufthansaSearchController — @RequestMapping("/internals/LUFTHANSA/search")

메서드HTTP 매핑경로호출 서비스 메서드
search@PostMapping/internals/LUFTHANSA/searchLufthansaFlightSearchService.search
detail@GetMapping/internals/LUFTHANSA/searchgetFareItinerary + FlightAmenityService.findAmenityMap
reissueSearch@PostMapping("/reissue").../search/reissuereissueSearch
reissueDetail@GetMapping("/reissue").../search/reissuegetFareItinerary

서킷브레이커는 search에만

search@CircuitBreaker(name = "lufthansaSearch", fallbackMethod = "searchFallback")로 보호된다(LufthansaSearchController.kt:28). OPEN 시 빈 리스트를 반환하고 Datadog 스팬에 supplier.circuit-breaker=OPEN 태그를 남긴다. 이것이 이 시스템의 “이벤트/상태 전파”가 메시지큐가 아니라 Resilience4j 상태전이 + 스팬 태깅으로 구현되는 한 예다(resilience-and-events).

4.2 LufthansaBookingController — @RequestMapping("/internals/LUFTHANSA/bookings")

메서드HTTP 매핑경로호출 서비스
create@PostMapping/bookingsLufthansaBookingService.book
changeApis@PutMapping("/{pnr}")/bookings/{pnr}LufthansaPassengerService.changeApis
retrieve@GetMapping("/{pnr}")/bookings/{pnr}bookingService.retrieve
cancel@PutMapping("/{pnr}/cancel")/bookings/{pnr}/cancelLufthansaCancelService.cancel
expectedCancel@GetMapping("/{pnr}/expected-cancel")/bookings/{pnr}/expected-cancelcancelService.expectedCancel
cancelable@GetMapping("/{pnr}/cancelable")/bookings/{pnr}/cancelablecancelService.cancelable
divide@PostMapping("/{pnr}/divide")/bookings/{pnr}/dividebookingService.divide
checkPnr@GetMapping("/{pnr}/check-pnr")/bookings/{pnr}/check-pnr(항상 true 반환)
confirm@GetMapping("/{pnr}/confirm")/bookings/{pnr}/confirmbookingService.retrieve
repricing@GetMapping("/{pnr}/repricing")/bookings/{pnr}/repricingbookingService.retrieveRepricingView

divide는 코드만 존재, 실제로는 미지원

LufthansaClient.divide(infrastructure/LufthansaClient.kt:425)의 KDoc은 명시한다: “LH 루프트한자는 divide API 기능을 지원하지 않습니다.” 호출하면 NDC가 Code="325" ... functionality has not been enabled for this carrier 오류를 반환한다. 컨트롤러까지 구현돼 있어 신입이 “되는 줄” 알기 쉬운 함정 → lufthansa-pitfalls.

4.3 LufthansaTicketingController — @RequestMapping("/internals/LUFTHANSA/ticketing")

메서드HTTP 매핑경로호출 서비스
ready@PostMapping("/ready")/ticketing/readyLufthansaTicketingService.ready
issue@PostMapping/ticketingticketingService.issue (사전결제 prepayment만 허용)
reissue@PostMapping("/addition")/ticketing/additionticketingService.reissueRedis 폴링 키 반환(202 ACCEPTED)
checkReissue@GetMapping("/addition/{reissueKey}")/ticketing/addition/{reissueKey}poller(...) 폴링 결과 조회

재발행은 비동기 폴링 패턴

재발행은 시간이 오래 걸려 동기 응답하지 않는다. reissuepolling(...)으로 작업을 띄우고 즉시 폴링 키를 202 ACCEPTED로 돌려준 뒤, 클라이언트가 checkReissuePENDING/COMPLETE/ERROR를 폴링한다(support/utilpolling/poller, async-coroutines).

4.4 LufthansaFareRuleController — @RequestMapping("/internals/LUFTHANSA/fare-rules")

메서드HTTP 매핑경로호출 서비스
getFareRules@GetMapping/fare-rulesLufthansaFareRuleService.findFareRules
getStructuredFareRules@GetMapping("/structured")/fare-rules/structuredgetFareItineraryStructuredFareRuleView

4.5 LufthansaAncillaryController — @RequestMapping("/internals/LUFTHANSA/ancillaries")

메서드HTTP 매핑경로호출 서비스
searchAvail(key)@GetMapping("/avail/key")/ancillaries/avail/keyancillaryService.searchAvailAncillary(key, ...)
searchAvail(pnr)@GetMapping("/avail/pnr")/ancillaries/avail/pnrsearchAvailAncillary(supplierIdentificationKey, validatingCarrier)
searchExtraBaggages(key)@GetMapping("/baggage/key")/ancillaries/baggage/keysearchExtraBaggages(key, ...)
searchExtraBaggages(pnr)@GetMapping("/baggage/pnr")/ancillaries/baggage/pnrsearchExtraBaggages(supplierIdentificationKey, ...)

application 서비스 목록 (7종)

LufthansaFlightSearchService, LufthansaBookingService, LufthansaTicketingService, LufthansaCancelService, LufthansaPassengerService, LufthansaAncillaryService, LufthansaFareRuleService.

컨트롤러 5 vs 서비스 7 — 왜 개수가 다른가

취소(LufthansaCancelService)·승객변경(LufthansaPassengerService)은 별도 컨트롤러 없이 LufthansaBookingController에 흡수되어 노출된다. “1 컨트롤러 = 1 서비스”가 아님에 주의. 서비스 간에도 호출이 일어난다(예: AncillaryService/FareRuleService/TicketingService가 모두 FlightSearchService 또는 BookingService를 주입받아 재사용).

지원 오퍼레이션 매핑

오퍼레이션지원NDC 메시지(action)근거
SearchOAirShoppingRQAirShoppingRSLufthansaClient.search
BookingOOfferPriceRQ(선행) + OrderCreateRQOrderViewRSpricing + booking
TicketingOOrderChangeRQ(payment) → OrderViewRSsavePayment; 재발행 OrderReshopRQ+OrderChangeRQ
FareRuleOOfferPriceRQOfferPriceRSgetFareRule(가격조회 응답에서 규정 추출)
AncillaryOServiceListRQ/SeatAvailabilityRQ + OrderChangeRQ좌석·추가수하물 검색/구매
(Queue / CashReceipt)XGDS 전용. NDC인 Lufthansa에는 없음

5. 중요도 별점

중요도: ★★☆ (3점 만점 중 2점)

근거

  • (+) NDC EDIST의 대표 학습 표본: 11개 모듈 중 NDC를 가장 완성도 높게(검색·예약·발권·재발행·부가서비스·환불계산) 구현. koreanair(NDC V21.3)·singaporeair(EDIST 18.1)와 비교 학습하기 좋은 기준점이다.
  • (+) 프로토콜 난이도 상위: “NDC를 SOAP으로 감싸는” 하이브리드, OrderChangeRQ/OrderReshopRQ의 다목적 재사용, 비동기 재발행 폴링, 발권 실패 시 자동취소 코루틴 등 고급 패턴이 한 모듈에 응축.
  • (−) 규모는 중하위: 파일 87개로 GDS 3사(amadeus 873 / sabre 882 / galileo 525)나 amadeusndc(357) 대비 작다. 운영 트래픽·핵심도에서 GDS만큼은 아니다.
  • (−) 일부 기능 미지원: divide 미지원, cancel 반환값 무력화(0L 고정, TODO 주석 다수) 등 미완·우회 흔적이 있어 “전체 그림”보다 “패턴 학습”에 가치가 있다.

결론: 운영 핵심도는 보통이지만, NDC를 배우는 입문자에게는 ★★★급 교재. GDS를 보기 전 NDC 감을 잡는 용도로 강력 추천.


연습 문제

Q1. Lufthansa는 NDC인데 왜 LufthansaClient가 SOAP 헤더와 봉투를 만들까?

Q2. 취소(cancel) 요청을 받았을 때 곧바로 OrderCancelRQ를 보내지 않는다. 그 앞에 무엇을 먼저 하나?

Q3. LufthansaTicketingService.issue가 발권 중 예외를 만나면 어떤 일이 추가로 일어나는가?


더 보기