Korean Air(대한항공, 항공사 코드 KE)는 NDC(New Distribution Capability) V21.3 표준으로 연동하는 FSC(Full Service Carrier)다. GDS를 거치지 않고 항공사가 직접 발행하는 IATA NDC 메시지(IATA_AirShoppingRQ, IATA_OrderCreateRQ, IATA_OrderChangeRQ, IATA_OrderReshopRQ, IATA_OrderRetrieveRQ, IATA_OfferPriceRQ)를 사용하지만, 전송 자체는 그 NDC 메시지를 SOAP 봉투로 한 번 더 감싸 보내는 하이브리드 구조다. 결제는 별도 결제 게이트웨이(NicePay)에 raw TCP 소켓 + SEED 암호화로 직접 붙는다.
Supplier.KOREANAIR로 식별되며, 단일 항공사(KE)를 NDC 표준으로 직접 연동한다. GDS(Amadeus·Sabre·Galileo)의 항공사-중립 메시지가 아니라, IATA가 표준화한 Offers & Orders 메시지 모델을 그대로 사용한다. NDC 버전은 코드에 박제되어 있다.
네임스페이스도 IATA EASD 2015(Offers & Orders) 스키마를 가리킨다.
// infrastructure/request/KoreanairNameSpace.kt:3-7object KoreanairNameSpace { const val OFFERS_AND_ORDERS_MESSAGE = "http://www.iata.org/IATA/2015/EASD/00/IATA_OffersAndOrdersMessage" const val OFFERS_AND_ORDERS_MESSAGE_COMMON_TYPES = "http://www.iata.org/IATA/2015/EASD/00/IATA_OffersAndOrdersCommonTypes"}
"NDC인데 SOAP를 쓴다" — 가장 먼저 알아야 할 사실
메시지 페이로드(IATA_AirShoppingRQ 등)는 순수 NDC/XML이지만, 실제 와이어로 나가는 전문은 SOAP 봉투에 NDC XML을 통째로 끼워 넣은 형태다. KoreanairClient의 soapRequestBodyConverter(infrastructure/KoreanairClient.kt:549-593)가 그 봉투를 만든다.
즉 KE 연동은 TOPAS(1A) 경유 NDC 게이트웨이를 SOAP로 호출하는 구조다(SOAP Header의 script 엔진이 FLXDM, 스크립트명 nol_universe-ke-dispatch.flxdm — KoreanairClient.kt:570-573). 봉투/인증 헤더 구조 전체는 koreanair-protocol에서 다룬다.
KoreanairProperties.getApiProperties(configuration/Properties.kt:567-576)는 MDC에 담긴 SalesChannel/SalesFunnel로 channels → funnels를 찾아 KoreanairApiProperties(IATA코드·userName·password·pseudoCityCode·NicePay 등)를 고른다. 즉 같은 KE라도 판매 채널/퍼널마다 다른 IATA·PCC·NicePay 단말로 호출될 수 있다. 채널이 없으면 NOT_SUPPORTED_SALES_CHANNEL, 퍼널이 없으면 NOT_SUPPORTED_SALES_FUNNEL 예외다.
1-3. 왜 결제는 SOAP가 아니라 raw 소켓인가
발권(issue)/취소 환불 시 카드 승인은 NDC 메시지가 아니라 NicePay 결제 게이트웨이에 직접 붙는다. KoreanairPaymentClient(infrastructure/KoreanairPaymentClient.kt)는 HTTP가 아니라 java.net.Socket으로 호스트:포트에 TCP 연결하고, 요청 본문을 SEED 암호화 + EUC-KR로 직렬화해 보낸다(serializeToLiteralTextByByte, charset EUC-KR, KoreanairPaymentClient.kt:95-100). 타임아웃 5초(timeOut = 5000)이며 \r이 나올 때까지 응답을 읽는다(KoreanairPaymentClient.kt:108-115). 이 결제 채널은 NDC와 완전히 분리된 별도 인프라다 — 자세한 위험은 koreanair-pitfalls 참조.
2. 모듈 규모와 서브패키지 구조
koreanair 모듈은 213개 .kt 파일 / 약 8,528 LOC로, 11개 공급사 중 중상위 규모다(GDS 3대장 amadeus 873·sabre 882·galileo 525 다음, NDC/LCC 군에서는 amadeusndc 357 다음으로 큰 편). 규모의 대부분은 NDC 스키마를 그대로 옮긴 DTO(infrastructure/request 79개 + infrastructure/response 95개 = 174개, 전체의 약 82%)에서 온다.
파일 수에 압도되지 말 것. 174개 DTO는 NDC 스키마의 기계적 미러이므로 한 번에 다 읽을 필요가 없다. 흐름을 잡으려면 ① 컨트롤러(interfaces/controller/internals) → ② 서비스(application) → ③ KoreanairClient의 해당 메서드 → ④ 필요할 때만 infrastructure/request|response의 IATA_*RQ/RS 진입점만 보면 된다. 매핑 로직은 응답 DTO의 toBooking() / toFareItineraries() / toPricingInfo() 확장 함수에 집중되어 있다.
FareItinerary/CancelableTypeDetail 전용 RedisTemplate
GzipRedisSerializer
OrderChangeRQ 하나가 6가지 일을 한다
NDC의 IATA_OrderChangeRQ는 “주문 변경” 단일 메시지로, 발권(ofIssue)·발권후취소(ofIssuedCancel)·발권전취소(ofUnIssuedCancel)·연락처변경(ofContactInfo)·재발행(ofReissue)·PNR분리(ofSplit)를 모두 같은 메시지 타입으로 표현한다(OrderChangeRQ.kt:37-119의 6개 팩토리). GDS처럼 오퍼레이션마다 다른 전문이 있는 게 아니라 하나의 OrderChange에 의도(intent)를 담아 분기하는 것이 NDC Order Management 모델의 핵심이다. 어떤 팩토리가 어떤 NDC 파라미터를 채우는지는 koreanair-operations에서 다룬다.
4. 공개 인터페이스 (컨트롤러 엔드포인트 + application 서비스)
4-1. 컨트롤러와 엔드포인트
중앙 디스패처 없이, 모듈 자체 REST 컨트롤러가 Triple 예약 시스템의 내부 API(/internals/KOREANAIR/...)로 노출된다.
Booking/Ticketing 계열은 경로변수 {pnr}을 받지만, 내부에서 실제 NDC 호출에 쓰는 키는 대부분 supplierIdentificationKey(= NDC OrderID)다. 예: retrieve는 bookingService.retrieve(orderId = supplierIdentificationKey)를 호출하고 {pnr}은 사용하지 않는다(KoreanairBookingController.kt:99-108). PNR과 OrderID의 관계는 koreanair-operations에서 다룬다.
재발행은 동기 응답이 아니다 — 폴링 패턴
발권(/ticketing)은 동기 응답이지만, 재발행(/ticketing/addition)은 202 ACCEPTED로 즉시 pollingKey만 돌려준다(KoreanairTicketingController.kt:52-67). 실제 재발행은 Redis 기반 polling이 백그라운드로 수행하고, 클라이언트는 /ticketing/addition/{reissueKey}로 PENDING/COMPLETE/ERROR 상태를 폴링한다(poller, KoreanairTicketingController.kt:69-85). 게다가 재발행 직후 수하물 정보 누락 이슈로 5초 delay 후 retrieve하는 보정 로직이 있다(KoreanairTicketingService.reissue 113-122행). async-coroutines·koreanair-pitfalls 참조.
지원 오퍼레이션은 Search · Booking · Ticketing · FareRule 4종이다(Ancillary·Queue·CashReceipt는 미지원). Booking 안에 취소/분리/APIS변경/재발행 후보검색 같은 부가 동작이 포함되고, Ticketing 안에 재발행과 결제가 통합되어 있다.
flowchart TD
T["Triple 예약시스템"] -->|"POST /internals/KOREANAIR/{op}"| C["Controller<br/>DTO ↔ support.model 변환"]
C -->|"CircuitBreaker koreanairSearch 검색만<br/>OPEN 시 emptyList"| CB(["OPEN 시 emptyList 반환"])
C --> S["Service<br/>pricing / 보상 트랜잭션 / 캐싱 / 스코어링"]
S --> KC["KoreanairClient"]
S --> PC["KoreanairPaymentClient"]
S --> R["Redis<br/>FareItinerary / CancelableTypeDetail / 재발행 폴링키 gzip"]
KC -->|"SOAP 봉투 NDC XML<br/>헤더 Ocp-Apim-Subscription-Key + SOAP iden"| GW["TOPAS 1A NDC 게이트웨이 KE"]
PC -->|"TCP 소켓 SEED/EUC-KR"| NP["NicePay 결제 GW"]
Controller에는 검색 오퍼레이션에만 @CircuitBreaker(name="koreanairSearch")가 적용되며, 회로 OPEN 시 emptyList()를 반환한다.
Controller ↔ Service 사이에서 DTO와 support.model 변환이 일어난다.
왜 ★★★인가: 단순 LCC REST 연동(예: jejuair)과 달리, KE 모듈은 ① IATA NDC 표준 이해, ② SOAP 봉투/TOPAS 게이트웨이 인증, ③ Order Management의 “단일 OrderChange + 의도 분기” 모델, ④ 발권 실패 시 결제취소+예약취소 보상 트랜잭션(KoreanairTicketingService.issue 89-96행의 catch 블록), ⑤ 재발행 비동기 폴링, ⑥ NDC와 분리된 NicePay raw 소켓 결제까지 이질적인 패턴이 한 모듈에 집약된다. NDC 입문 모듈로서 학습 가치가 가장 높은 동시에, 운영 시 정합성 깨짐 위험도 크다.
5분 자가진단
아래 질문에 답해보고 막히면 본문/관련 노트로 돌아가자.
KE는 NDC를 쓰는데 왜 HTTP 헤더 Content-Type이 text/xml이고 SOAPAction이 붙는가?
발권(/ticketing)과 재발행(/ticketing/addition)의 HTTP 응답 코드/패턴이 다른 이유는?
카드 결제는 어느 클라이언트가, 어떤 프로토콜로 처리하는가?
[!answer]- 정답 보기
NDC 메시지(IATA_*RQ)를 그대로 보내지 않고 SOAP 봉투의 XXTransaction/REQ 안에 byte로 주입해서 TOPAS(1A) NDC 디스패치(FLXDM 스크립트)를 호출하기 때문이다. 봉투를 만드는 곳은 KoreanairClient.soapRequestBodyConverter(549-593행), 헤더는 getHeaderMap(80-92행).
발권은 동기(200 OK)지만 재발행은 처리가 길고 수하물 보정 delay(5000)까지 필요해, 202 ACCEPTED로 pollingKey만 주고 Redis 기반 polling/poller로 비동기 처리한다(KoreanairTicketingController.kt:52-85).
NDC가 아니라 KoreanairPaymentClient가 java.net.Socket으로 NicePay에 직접 TCP 연결하고, 본문을 SEED 암호화 + EUC-KR로 직렬화해 전송한다(KoreanairPaymentClient.kt:83-126).
다음으로 읽을 노트
koreanair-operations — 오퍼레이션별 시퀀스(검색→pricing→예약→발권→재발행→취소)와 OrderChangeRQ 6개 팩토리의 의미
koreanair-protocol — SOAP 봉투/인증 헤더, NDC 메시지 매핑(toBooking/toFareItineraries), NicePay SEED 전문