Korean Air — 개요

module-koreanair arch-supplier-module api-ndc

한 줄 요약

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 암호화로 직접 붙는다.

관련 노트: 오퍼레이션 상세 · 전문 구조 · 주의점 · 시스템 아키텍처 · 요청 흐름


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

1-1. NDC인가? FSC인가? 코드로 검증

Supplier.KOREANAIR로 식별되며, 단일 항공사(KE)를 NDC 표준으로 직접 연동한다. GDS(Amadeus·Sabre·Galileo)의 항공사-중립 메시지가 아니라, IATA가 표준화한 Offers & Orders 메시지 모델을 그대로 사용한다. NDC 버전은 코드에 박제되어 있다.

// infrastructure/request/PayloadAttribute.kt:13-17
@JacksonXmlProperty(
    localName = "VersionNumber",
    namespace = KoreanairNameSpace.OFFERS_AND_ORDERS_MESSAGE_COMMON_TYPES
)
val version: String = "21.3",

네임스페이스도 IATA EASD 2015(Offers & Orders) 스키마를 가리킨다.

// infrastructure/request/KoreanairNameSpace.kt:3-7
object 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을 통째로 끼워 넣은 형태다. KoreanairClientsoapRequestBodyConverter(infrastructure/KoreanairClient.kt:549-593)가 그 봉투를 만든다.

// infrastructure/KoreanairClient.kt:577-582 — NDC 메시지를 SOAP Body의 REQ 안에 byte로 주입
body {
    childElement("ns1:XXTransaction") {
        attribute("xmlns:ns1") { "http://www.w3.org/2001/XMLSchema" }
        childDocument("REQ", objectMapper.writeValueAsBytes(request).inputStream())
    }
}

즉 KE 연동은 TOPAS(1A) 경유 NDC 게이트웨이를 SOAP로 호출하는 구조다(SOAP Header의 script 엔진이 FLXDM, 스크립트명 nol_universe-ke-dispatch.flxdmKoreanairClient.kt:570-573). 봉투/인증 헤더 구조 전체는 koreanair-protocol에서 다룬다.

1-2. 비즈니스·시장 맥락

특성내용근거
항공사Korean Air (KE), 국적 FSCSupplier.KOREANAIR
연동 표준IATA NDC V21.3 (Offers & Orders)PayloadAttribute.kt:17, KoreanairNameSpace.kt
전송 방식NDC/XML을 SOAP 봉투로 감싼 HTTP POST (text/xml)KoreanairClient.getHeaderMap, soapRequestBodyConverter
인증Ocp-Apim-Subscription-Key(헤더) + SOAP Header iden(u/p/agt/…)KoreanairClient.kt:80-92, 552-563
게이트웨이TOPAS(1A) NDC 디스패치 (FLXDM 스크립트)KoreanairClient.kt:570-573
결제발권 시 NicePay 게이트웨이로 별도 카드승인(키인)KoreanairPaymentClient(raw socket + SEED)
채널/퍼널 인증 분기salesChannel/salesFunnel별 API 자격증명 선택KoreanairProperties.getApiProperties
시크릿 관리AWS Secrets Manager({env}/air-intl-adapter/koreanair)resources/supplier/koreanair.yml

자격증명은 "채널 × 퍼널" 조합으로 선택된다

KoreanairProperties.getApiProperties(configuration/Properties.kt:567-576)는 MDC에 담긴 SalesChannel/SalesFunnelchannels → 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%)에서 온다.

supplier/koreanair/
├── application/            (7) 오케스트레이션 서비스 — 핵심 로직
│   ├── KoreanairFlightSearchService.kt   검색·재발행검색·캐싱·스코어링
│   ├── KoreanairBookingService.kt        예약 생성·pricing·PNR 분리(split)
│   ├── KoreanairTicketingService.kt      발권·재발행(폴링)·결제/취소 보상 트랜잭션
│   ├── KoreanairCancelService.kt         발권전(VOID)/발권후 취소·환불계산 캐시
│   ├── KoreanairPaymentService.kt        NicePay 승인/취소 래퍼 + Slack 경보
│   ├── KoreanairPassengerService.kt      APIS/연락처 변경(diff 후 변경분만)
│   └── KoreanairFareRuleService.kt       운임규정 조회(pricing 응답에서 추출·캐시)
├── infrastructure/         (176) 외부 API 클라이언트 + NDC DTO
│   ├── KoreanairClient.kt                NDC SOAP 클라이언트(검색/예약/발권/취소/재발행/조회)
│   ├── KoreanairPaymentClient.kt         NicePay raw TCP 소켓 + SEED 암호화 결제
│   ├── request/  (79)                    IATA_*RQ + 하위 NDC 요청 DTO
│   │   └── nicepay/ (2)                   NicePayApproveRequest / NicePayCancelRequest
│   └── response/ (95)                     IATA_*RS + 하위 NDC 응답 DTO
│       └── nicepay/ (2)                   NicePayApproveResponse / NicePayCancelResponse
├── domain/                 (4) 도메인 모델 + Redis 리포지토리
│   ├── model/      FareItinerary.kt, CancelableTypeDetail.kt
│   └── repository/ KoreanairFareItineraryRepository.kt, KoreanairCancelableTypeDetailRepository.kt
├── interfaces/             (4) Triple 내부용 REST 컨트롤러
│   └── controller/internals/ Search / Booking / Ticketing / FareRule 컨트롤러
├── support/                (21) 공급사 전용 도메인/유틸/Enum
│   ├── model/  (14)  Booking, Order, Passenger, Payment, Schedule, Leg, Fare ...
│   ├── enums/  (5)   CabinCode, PassengerTypeCode, ServiceStatusCode, TicketStatusCode, KoreanairSoapHeaderNamespace
│   └── util/   (2)   AirportUtils(목적지 조합), FareItineraryScoring(검색결과 점수)
└── configuration/          (1) RedisConfiguration.kt (FareItinerary/CancelableTypeDetail 전용 RedisTemplate)

신입이 코드를 읽는 순서

파일 수에 압도되지 말 것. 174개 DTO는 NDC 스키마의 기계적 미러이므로 한 번에 다 읽을 필요가 없다. 흐름을 잡으려면 ① 컨트롤러(interfaces/controller/internals) → ② 서비스(application) → ③ KoreanairClient의 해당 메서드 → ④ 필요할 때만 infrastructure/request|responseIATA_*RQ/RS 진입점만 보면 된다. 매핑 로직은 응답 DTO의 toBooking() / toFareItineraries() / toPricingInfo() 확장 함수에 집중되어 있다.


3. 핵심 파일 표

파일(상대경로)역할비고
interfaces/controller/internals/KoreanairSearchController.kt검색/상세/재발행검색 진입점@CircuitBreaker(name="koreanairSearch") 적용
interfaces/controller/internals/KoreanairBookingController.kt예약 생성/조회/취소/분리/APIS변경가장 많은 엔드포인트(8개)
interfaces/controller/internals/KoreanairTicketingController.kt발권/재발행(폴링)재발행은 비동기 폴링(polling/poller + Redis)
interfaces/controller/internals/KoreanairFareRuleController.kt운임규정/구조화 운임규정 조회검색 캐시(FareItinerary)에 의존
application/KoreanairFlightSearchService.kt검색 오케스트레이션·캐싱·스코어링·국내경유 필터pmap/withBlocking 병렬 호출
application/KoreanairBookingService.ktpricing→예약, PNR split 검증(성인-유아 쌍)objectMapper로 fareItinerary 로깅
application/KoreanairTicketingService.kt발권 + 결제/예약 보상 트랜잭션, 재발행실패 시 결제취소·예약취소 비동기
application/KoreanairCancelService.kt발권전 VOID / 발권후 환불취소, 환불계산 캐시unissued로 분기
application/KoreanairPaymentService.ktNicePay 승인/취소 래퍼 + SlackKoreanairPaymentClient 위임
infrastructure/KoreanairClient.ktNDC SOAP 클라이언트(전 오퍼레이션)593 LOC, 모듈의 심장
infrastructure/KoreanairPaymentClient.ktNicePay raw 소켓 결제(SEED/EUC-KR)HTTP 아님, TCP 직결
infrastructure/request/AirShoppingRQ.kt검색 NDC 요청 루트(IATA_AirShoppingRQ)action = SOAPAction
infrastructure/request/OrderChangeRQ.kt발권/취소/연락처/재발행/분리 다목적 요청ofIssue·ofIssuedCancel·ofUnIssuedCancel·ofContactInfo·ofReissue·ofSplit
infrastructure/request/OrderReshopRQ.kt환불계산/재발행검색 요청ofRefundCalculate·ofReissueSearch
infrastructure/request/PayloadAttribute.ktNDC 버전(21.3)·TrxID 부착버전 박제 위치
support/model/Booking.kt예약 도메인(PNR·승객·스케줄·타임리밋)unissued 파생 프로퍼티
support/util/FareItineraryScoring.kt검색결과 정렬용 가중치 점수가격/비행시간/경유/시간대 가중
domain/repository/KoreanairFareItineraryRepository.kt검색결과 Redis Hash 캐시gzip 직렬화
configuration/RedisConfiguration.ktFareItinerary/CancelableTypeDetail 전용 RedisTemplateGzipRedisSerializer

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/...)로 노출된다.

컨트롤러메서드(@*Mapping)경로오퍼레이션
SearchController@PostMapping/internals/KOREANAIR/searchSearch(검색)
@GetMapping/internals/KOREANAIR/searchSearch(상세)
@PostMapping("/reissue")/internals/KOREANAIR/search/reissueSearch(재발행 후보 검색)
@GetMapping("/reissue")/internals/KOREANAIR/search/reissueSearch(재발행 상세)
BookingController@PostMapping/internals/KOREANAIR/bookingsBooking(예약 생성)
@PutMapping("/{pnr}")/internals/KOREANAIR/bookings/{pnr}Booking(APIS/연락처 변경)
@PutMapping("/{pnr}/cancel")/internals/KOREANAIR/bookings/{pnr}/cancelBooking(취소)
@GetMapping("/{pnr}/expected-cancel")/internals/KOREANAIR/bookings/{pnr}/expected-cancelBooking(예상 취소 수수료)
@GetMapping("/{pnr}/cancelable")/internals/KOREANAIR/bookings/{pnr}/cancelableBooking(취소 가능 여부)
@GetMapping("/{pnr}/confirm")/internals/KOREANAIR/bookings/{pnr}/confirmBooking(예약 확정 조회)
@GetMapping("/{pnr}")/internals/KOREANAIR/bookings/{pnr}Booking(조회/retrieve)
@PostMapping("/{pnr}/divide")/internals/KOREANAIR/bookings/{pnr}/divideBooking(PNR 분리/split)
TicketingController@PostMapping/internals/KOREANAIR/ticketingTicketing(발권)
@PostMapping("/addition")/internals/KOREANAIR/ticketing/additionTicketing(재발행 요청, 202 ACCEPTED)
@GetMapping("/addition/{reissueKey}")/internals/KOREANAIR/ticketing/addition/{reissueKey}Ticketing(재발행 결과 폴링)
FareRuleController@GetMapping/internals/KOREANAIR/fare-rulesFareRule(운임 규정)
@GetMapping("/structured")/internals/KOREANAIR/fare-rules/structuredFareRule(구조화 운임 규정)

경로에 {pnr}이 있어도 실제 식별자는 supplierIdentificationKey

Booking/Ticketing 계열은 경로변수 {pnr}을 받지만, 내부에서 실제 NDC 호출에 쓰는 키는 대부분 supplierIdentificationKey(= NDC OrderID)다. 예: retrievebookingService.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 참조.

4-2. application 서비스 목록

서비스책임주요 협력자
KoreanairFlightSearchService검색/재발행검색 오케스트레이션, 캐싱, 스코어링, 국내 경유공항 필터KoreanairClient, KoreanairFareItineraryRepository, FlightSearchKeyRepository, CityClient
KoreanairBookingServicepricing→예약, 예약 조회, PNR 분리(성인-유아 쌍 검증)KoreanairClient, KoreanairFlightSearchService, CityClient
KoreanairTicketingService발권/재발행, 결제·예약 보상 트랜잭션KoreanairClient, KoreanairPaymentService, KoreanairCancelService, SlackService
KoreanairCancelService발권전 VOID / 발권후 환불취소, 환불계산 캐시KoreanairClient, KoreanairCancelableTypeDetailRepository, SlackService
KoreanairPaymentServiceNicePay 승인/취소 래핑 + 실패 Slack 경보KoreanairPaymentClient, SlackService
KoreanairPassengerServiceAPIS/연락처 변경 — diff 계산 후 변경분만 호출KoreanairClient
KoreanairFareRuleServicepricing 응답에서 운임규정 추출·캐시KoreanairFlightSearchService, KoreanairClient, FareRuleRepository, CityClient

지원 오퍼레이션은 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()를 반환한다.
  • ControllerService 사이에서 DTO와 support.model 변환이 일어난다.
  • KoreanairClient → TOPAS(1A) NDC 게이트웨이 호출 헤더: Ocp-Apim-Subscription-Key + SOAP iden.
  • KoreanairPaymentClient는 HTTP가 아닌 raw TCP 소켓(SEED 암호화 + EUC-KR)으로 NicePay 결제 GW에 직결한다.

5. 중요도 별점

종합 중요도: ★★★ (높음)

평가축별점근거
비즈니스 임팩트★★★국적 FSC, 노선/수요 비중이 크고 발권·환불·재발행 전 라이프사이클을 직접 처리
코드 복잡도★★★213파일/8.5K LOC, NDC 스키마 174 DTO + 보상 트랜잭션 + 폴링 + raw 소켓 결제
학습 난이도★★★“NDC인데 SOAP 봉투”, Order Management 단일 메시지 다중 의도, 비동기 재발행, 별도 결제 인프라가 한 모듈에 공존
운영 리스크★★★결제↔발권 정합성(보상 트랜잭션), 타임리밋, 재발행 수하물 누락, NicePay 소켓 타임아웃 등 장애 표면적이 넓음

왜 ★★★인가: 단순 LCC REST 연동(예: jejuair)과 달리, KE 모듈은 ① IATA NDC 표준 이해, ② SOAP 봉투/TOPAS 게이트웨이 인증, ③ Order Management의 “단일 OrderChange + 의도 분기” 모델, ④ 발권 실패 시 결제취소+예약취소 보상 트랜잭션(KoreanairTicketingService.issue 89-96행의 catch 블록), ⑤ 재발행 비동기 폴링, ⑥ NDC와 분리된 NicePay raw 소켓 결제까지 이질적인 패턴이 한 모듈에 집약된다. NDC 입문 모듈로서 학습 가치가 가장 높은 동시에, 운영 시 정합성 깨짐 위험도 크다.

5분 자가진단

아래 질문에 답해보고 막히면 본문/관련 노트로 돌아가자.

  1. KE는 NDC를 쓰는데 왜 HTTP 헤더 Content-Typetext/xml이고 SOAPAction이 붙는가?
  2. 발권(/ticketing)과 재발행(/ticketing/addition)의 HTTP 응답 코드/패턴이 다른 이유는?
  3. 카드 결제는 어느 클라이언트가, 어떤 프로토콜로 처리하는가?

[!answer]- 정답 보기

  1. NDC 메시지(IATA_*RQ)를 그대로 보내지 않고 SOAP 봉투의 XXTransaction/REQ 안에 byte로 주입해서 TOPAS(1A) NDC 디스패치(FLXDM 스크립트)를 호출하기 때문이다. 봉투를 만드는 곳은 KoreanairClient.soapRequestBodyConverter(549-593행), 헤더는 getHeaderMap(80-92행).
  2. 발권은 동기(200 OK)지만 재발행은 처리가 길고 수하물 보정 delay(5000)까지 필요해, 202 ACCEPTEDpollingKey만 주고 Redis 기반 polling/poller로 비동기 처리한다(KoreanairTicketingController.kt:52-85).
  3. NDC가 아니라 KoreanairPaymentClientjava.net.Socket으로 NicePay에 직접 TCP 연결하고, 본문을 SEED 암호화 + EUC-KR로 직렬화해 전송한다(KoreanairPaymentClient.kt:83-126).

다음으로 읽을 노트

  • koreanair-operations — 오퍼레이션별 시퀀스(검색→pricing→예약→발권→재발행→취소)와 OrderChangeRQ 6개 팩토리의 의미
  • koreanair-protocol — SOAP 봉투/인증 헤더, NDC 메시지 매핑(toBooking/toFareItineraries), NicePay SEED 전문
  • koreanair-pitfalls — 보상 트랜잭션·타임리밋·재발행 수하물 누락·소켓 타임아웃 등 지뢰
  • system-architecture · request-flow — 어댑터 전체 아키텍처에서 KE의 위치