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.3 | src/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:40 | NDC 코드셋(PADIS) 직접 참조 |
검증 대상 항공사 LH, LX, OS | LufthansaCertificationTest.kt:190 | LHG 그룹(Lufthansa·SWISS·Austrian) |
NDC를 "SOAP으로" 부른다는 역설
순수 NDC는 보통 REST/JSON이지만, Lufthansa Partner API는 NDC EDIST XML 페이로드를 SOAP 봉투(
<soap:Envelope>)에 넣어 전송한다.LufthansaClient의soapRequestBodyConverter(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)
신입을 위한 독해 순서
- 컨트롤러 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 | 만능 변경 RQ | ofApis/ofPayment/ofDivide/ofReissue/ofBookAncillaries/ofPurchaseAncillaries 등 8개 팩토리가 한 메시지를 재사용 |
infrastructure/request/OrderReshopRQ.kt | 재가격 RQ | ofRefundCalculate(환불계산)·ofReissueSearch(재발행검색) 두 용도 공유 |
domain/model/LufthansaFlightSearch.kt (253줄) | 도메인 FareItinerary | Redis 캐시 단위. 점수/스케줄/운임 보유 |
support/util/FareItineraryScoring.kt | 정렬 점수 | 가격·비행시간 min/avg/max 기반 withScore |
configuration/RedisConfiguration.kt | Redis 빈 | lufthansaFareItineraryRedisTemplate + Gzip 직렬화(NDC 운임 객체가 크다) |
support/enums/LufthansaSoapHeaderNamespace.kt | SOAP 네임스페이스 | TRANSACTION 헤더 네임스페이스 정의 |
src/main/resources/supplier/lufthansa.yml | 환경 설정 | dev/qa/staging/prod별 AWS Secrets Manager에서 자격증명 로드 |
configuration/Properties.kt:346 (공용) | LufthansaProperties | channel/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/search | LufthansaFlightSearchService.search |
detail | @GetMapping | /internals/LUFTHANSA/search | getFareItinerary + FlightAmenityService.findAmenityMap |
reissueSearch | @PostMapping("/reissue") | .../search/reissue | reissueSearch |
reissueDetail | @GetMapping("/reissue") | .../search/reissue | getFareItinerary |
서킷브레이커는 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 | /bookings | LufthansaBookingService.book |
changeApis | @PutMapping("/{pnr}") | /bookings/{pnr} | LufthansaPassengerService.changeApis |
retrieve | @GetMapping("/{pnr}") | /bookings/{pnr} | bookingService.retrieve |
cancel | @PutMapping("/{pnr}/cancel") | /bookings/{pnr}/cancel | LufthansaCancelService.cancel |
expectedCancel | @GetMapping("/{pnr}/expected-cancel") | /bookings/{pnr}/expected-cancel | cancelService.expectedCancel |
cancelable | @GetMapping("/{pnr}/cancelable") | /bookings/{pnr}/cancelable | cancelService.cancelable |
divide | @PostMapping("/{pnr}/divide") | /bookings/{pnr}/divide | bookingService.divide |
checkPnr | @GetMapping("/{pnr}/check-pnr") | /bookings/{pnr}/check-pnr | (항상 true 반환) |
confirm | @GetMapping("/{pnr}/confirm") | /bookings/{pnr}/confirm | bookingService.retrieve |
repricing | @GetMapping("/{pnr}/repricing") | /bookings/{pnr}/repricing | bookingService.retrieve → RepricingView |
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/ready | LufthansaTicketingService.ready |
issue | @PostMapping | /ticketing | ticketingService.issue (사전결제 prepayment만 허용) |
reissue | @PostMapping("/addition") | /ticketing/addition | ticketingService.reissue → Redis 폴링 키 반환(202 ACCEPTED) |
checkReissue | @GetMapping("/addition/{reissueKey}") | /ticketing/addition/{reissueKey} | poller(...) 폴링 결과 조회 |
재발행은 비동기 폴링 패턴
재발행은 시간이 오래 걸려 동기 응답하지 않는다.
reissue는polling(...)으로 작업을 띄우고 즉시 폴링 키를202 ACCEPTED로 돌려준 뒤, 클라이언트가checkReissue로PENDING/COMPLETE/ERROR를 폴링한다(support/util의polling/poller, async-coroutines).
4.4 LufthansaFareRuleController — @RequestMapping("/internals/LUFTHANSA/fare-rules")
| 메서드 | HTTP 매핑 | 경로 | 호출 서비스 |
|---|---|---|---|
getFareRules | @GetMapping | /fare-rules | LufthansaFareRuleService.findFareRules |
getStructuredFareRules | @GetMapping("/structured") | /fare-rules/structured | getFareItinerary → StructuredFareRuleView |
4.5 LufthansaAncillaryController — @RequestMapping("/internals/LUFTHANSA/ancillaries")
| 메서드 | HTTP 매핑 | 경로 | 호출 서비스 |
|---|---|---|---|
searchAvail(key) | @GetMapping("/avail/key") | /ancillaries/avail/key | ancillaryService.searchAvailAncillary(key, ...) |
searchAvail(pnr) | @GetMapping("/avail/pnr") | /ancillaries/avail/pnr | searchAvailAncillary(supplierIdentificationKey, validatingCarrier) |
searchExtraBaggages(key) | @GetMapping("/baggage/key") | /ancillaries/baggage/key | searchExtraBaggages(key, ...) |
searchExtraBaggages(pnr) | @GetMapping("/baggage/pnr") | /ancillaries/baggage/pnr | searchExtraBaggages(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) | 근거 |
|---|---|---|---|
| Search | O | AirShoppingRQ → AirShoppingRS | LufthansaClient.search |
| Booking | O | OfferPriceRQ(선행) + OrderCreateRQ → OrderViewRS | pricing + booking |
| Ticketing | O | OrderChangeRQ(payment) → OrderViewRS | savePayment; 재발행 OrderReshopRQ+OrderChangeRQ |
| FareRule | O | OfferPriceRQ → OfferPriceRS | getFareRule(가격조회 응답에서 규정 추출) |
| Ancillary | O | ServiceListRQ/SeatAvailabilityRQ + OrderChangeRQ | 좌석·추가수하물 검색/구매 |
| (Queue / CashReceipt) | X | — | GDS 전용. 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 헤더와 봉투를 만들까?정답 보기
Lufthansa Partner API가 NDC EDIST(XML) 페이로드를 SOAP 트랜스포트로 감싸 받기 때문이다.
soapRequestBodyConverter(infrastructure/LufthansaClient.kt:866)가 NDC RQ 객체를 직렬화해<ns1:XXTransaction>바디 안REQ도큐먼트로 넣고, SOAP 헤더에<iden>(u/p/pseudocity/agt…)와FLXDM스크립트 정보를 붙인다. 응답도soapBodyDeserializerOf로 SOAP Body를 벗긴 후 NDC 엘리먼트를 역직렬화한다. 표준=NDC, 전송=SOAP.
Q2. 취소(cancel) 요청을 받았을 때 곧바로
OrderCancelRQ를 보내지 않는다. 그 앞에 무엇을 먼저 하나?정답 보기
LufthansaCancelService.cancel은 먼저expectedCancel을 호출해OrderRetrieveRQ로 예약을 조회하고,OrderReshopRQ(ofRefundCalculate)로 환불 가능 여부(VOID/REFUND)와 환불액을 선계산한다(application/LufthansaCancelService.kt:38-43,infrastructure/LufthansaClient.kt:458 refundCalculate). 그 결과로 VOID인지 REFUND인지 판단한 뒤에야OrderCancelRQ를 보낸다. 즉 취소는 “조회 → 재가격(reshop) → 취소”의 3단계다.
Q3.
LufthansaTicketingService.issue가 발권 중 예외를 만나면 어떤 일이 추가로 일어나는가?정답 보기
savePayment호출이 실패하면catch블록에서cancelAsync(...)를 호출한다(application/LufthansaTicketingService.kt:53-59). 이는CoroutineScope(Dispatchers.IO).withLaunch { delay(5000); lufthansaClient.cancel(...) }로 5초 뒤 비동기 자동취소를 수행하고, 그 취소마저 실패하면SlackService.sendCancelFail로 Slack 경보를 보낸다. 발권 결제는 성공했는데 후속이 실패한 “고아 예약”을 막기 위한 보상 트랜잭션이며, 이 시스템의 상태 전파가 큐가 아니라 코루틴 + Slack 경보로 이뤄짐을 보여준다(async-coroutines, resilience-and-events).
더 보기
- 오퍼레이션별 상세 흐름·메시지 매핑 → lufthansa-operations
- NDC/EDIST/SOAP 프로토콜 디테일과 인증(certification) → lufthansa-protocol
- divide 미지원·null 삭제·취소 반환값 무력화 등 함정 → lufthansa-pitfalls
- 전체 모듈 공통 아키텍처 → system-architecture · 요청 한 바퀴 → request-flow
- 기존 분석 문서 → 싱가포르항공 NDC 분석(EDIST 비교 학습용)