T’way Air — 개요

module-tway arch-supplier pattern-lcc-rest api-rest config-seed

한 줄 요약

T’way Air(코드명 TWAY)는 국내 LCC(저비용항공사)로, GDS/NDC를 거치지 않고 항공사 자체 REST 엔드포인트에 직접 XML로 호출하는 모듈이다. 인증은 GDS 세션 토큰이 아니라 매 요청마다 HTTP Basic 헤더에 SEED 암호화된 비밀번호를 실어 보내는 stateless 방식이며, 결제·카드·PNR 같은 민감정보도 TwaySEED.jarSeedUtil.encoding()으로 암호화한다. Amadeus 같은 stateful PNR 세션이 없으므로 모듈 자체는 단순하지만, 요청/응답 DTO 수가 200개가 넘어(78 request + 127 response) 매핑 코드가 가장 큰 비중을 차지한다.


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

1-1. LCC / FSC / GDS / NDC 분류 → LCC + 직접 REST

T’way Air(티웨이항공)는 대한민국 국적 **LCC(Low-Cost Carrier)**다. GDS(Amadeus/Sabre/Galileo)나 NDC(Korean Air/Lufthansa) 채널을 경유하지 않고, 항공사가 자체 운영하는 예약 시스템(Navitaire 계열 SkySpeed/New Skies 류의 LCC PSS)의 REST API에 직접 붙는다. 코드로 확인되는 근거:

  • 모든 외부 호출이 infrastructure/TwayClient.kt에서 "${endpoint}/shop/..." 또는 "${endpoint}/book/..." 같은 REST 경로로 나간다 (TwayClient.kt:56, 89, 153, 183, 221, 271, 305, 325 …).
  • 요청 본문은 SOAP 봉투가 아니라 순수 XML이다. TwayClient의 헤더가 Content-Type: application/xml 하나뿐이다.
// TwayClient.kt:45-47
private val headerMap = mapOf(
    HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_XML_VALUE
)

GDS와의 결정적 차이

Amadeus/Sabre/Galileo는 SOAP 세션을 열고 PNR 상태를 서버가 들고 있는 stateful 구조다. T’way는 세션 개념이 없고, 매 호출이 독립적인 stateless REST 호출이다. 따라서 TwayClient에는 로그인/세션토큰/시그너처 같은 상태가 없으며, 인증 정보를 요청마다 새로 만들어 헤더에 붙인다(아래 1-3).

1-2. 비즈니스·시장 맥락 — 직항만 판매, 노선 화이트리스트

LCC 특성이 코드에 그대로 드러난다.

  • 직항만 판매한다. TwayClient.toItineraries()에서 경유 편명(segmentReferenceInfo가 2개 이상)을 명시적으로 걸러낸다.

    // TwayClient.kt:1043-1044
    val segmentReferenceInfo =
        pricingInfo.segmentReferenceInfos.first() // segmentReferenceInfo가 2개 이상인 것은 경유 편명이다. TW는 직항만 팔아야 한다.
    pricingInfo.tripRefIndex == tripInfo.tripIndex && pricingInfo.segmentReferenceInfos.size == 1 && ...
  • 취항 노선을 사전에 화이트리스트로 관리한다. 검색 전에 RetrieveRoute로 전 노선을 받아 Redis에 캐싱하고(TwayRouteService.getAllRoutes()), 요청 노선이 화이트리스트에 없으면 공급사 호출 없이 빈 결과로 단락(short-circuit) 한다.

    // TwayRouteService.kt:42-50
    if (availableRoutes.contains("${origin}_${destination}")) { OriginDestination(...) } else null
    // TwayFlightSearchService.kt:52-55
    val filteredOriginDestinations = twayRouteService.makeOriginDestinationsFilterByRoutes(originDestinationInfos)
    if (filteredOriginDestinations.isEmpty()) { return emptyList() }

1-3. 인증 — SEED 암호화 + HTTP Basic (TwaySEED.jar)

핵심: SEED는 KISA 국산 블록암호다

T’way 연동의 가장 중요한 특징이자 신입이 놓치기 쉬운 지점. T’way는 외부 라이브러리 libs/TwaySEED.jar(클래스 com.twayair.security.seed.SeedUtil)를 통해 비밀번호·카드정보·PNR 등 민감 데이터를 SEED(국산 128bit 블록암호)로 암호화해서 보낸다. 이 jar는 gradle에 로컬 파일 의존성으로 박혀 있다.

// build.gradle.kts:67
implementation(files("libs/TwaySEED.jar"))

인증 흐름은 GDS의 토큰 발급/세션 로그인이 아니라, 매 요청마다 HTTP Basic 헤더에 (agencyCode, SEED(password))를 실어 보내는 방식이다.

// TwayClient.kt:57-59 (모든 오퍼레이션이 동일 패턴 반복)
.post(request)
.authenticate(twayApiProperties.agencyCode, SeedUtil.encoding(twayApiProperties.password))
.header(headerMap)
// support/web/ClientSupport.kt:131-134 — authenticate는 결국 Authorization: Basic 헤더다
fun authenticate(userName: String, password: String): OkHttpRequestBuilder {
    this.header.put("Authorization", Credentials.basic(userName, password))
    return this
}

SEED 암호화 대상은 비밀번호뿐 아니라 다음까지 확장된다.

대상위치비고
API 비밀번호 (password)TwayClient 전 메서드 (SeedUtil.encoding(password))Basic 헤더
결제 비밀번호 (paymentPassword)TwayClient.kt:602, 645발권/부가서비스 결제
카드번호·유효기간·생년월일/사업자번호·카드비밀번호infrastructure/request/CardInfo.kt:24-32OfflineAuthInfo (오프라인 인증)
부가서비스 토큰 발급용 PNR·대리점코드infrastructure/ancillary/TwayAncillaryIssueTokenRequest.kt:14-15, TwayAncillaryClient.kt:85-90딥링크 토큰

SeedUtil(외부 jar) ≠ SeedEncryptor(adapter 내부)

어댑터에는 support/util/SeedEncryptor.kt@SeedEncrypt 애너테이션도 따로 존재하지만, 이것은 Korean Air의 NicePay 결제(KoreanairPaymentClient)용이며 T’way와는 무관하다. T’way가 쓰는 SEED는 오직 com.twayair.security.seed.SeedUtil(외부 jar)이다. 두 SEED 구현을 혼동하지 말 것 → 자세한 함정은 tway-pitfalls.


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

supplier/tway/                          (총 253 .kt 파일 / 약 10,659 LOC)
├── application/            (10 파일, ~1,009 LOC) — 서비스 계층
│   ├── TwayFlightSearchService.kt      검색/재발행검색/숨은경유 합성
│   ├── TwayRouteService.kt             취항노선 화이트리스트 캐싱
│   ├── TwayBookingService.kt           예약/조회/취소취합/분리(divide)
│   ├── TwayPricingService.kt           운임확정(FareQuote)
│   ├── TwayPassengerService.kt         APIS(여권)정보 변경
│   ├── TwayTicketingService.kt         발권/재발행(reissue)/타임아웃 슬랙
│   ├── TwayCancelService.kt            취소/예상취소/취소가능여부(void vs refund)
│   ├── TwayFareRuleService.kt          운임규정 조회
│   ├── TwayAncillaryService.kt         부가서비스(수하물/좌석/딥링크/해제)
│   └── TwayAgencyCreditService.kt      대리점 크레딧 잔액 조회
├── configuration/          (1 파일, 29 LOC)
│   └── RedisConfiguration.kt           FareItinerary Gzip 직렬화 RedisTemplate
├── domain/                 (3 파일, 255 LOC)
│   ├── model/TwayFlightSearch.kt       FareItinerary / Leg 등 도메인 모델
│   └── repository/                     TwayFareItineraryRepository, TwayRouteRepository
├── infrastructure/         (210 파일, ~8,040 LOC) ★ 모듈의 절대 다수
│   ├── TwayClient.kt                   ★ 단일 외부 API 클라이언트 (1,089 LOC, 모든 호출 집중)
│   ├── TwayPaymentError.kt             결제 에러코드 → ErrorMessage 매핑
│   ├── ancillary/          (3 파일)    딥링크 토큰 발급 전용 클라이언트
│   ├── request/            (78 파일)   *RQ / *RQmsg XML 요청 DTO
│   └── response/           (127 파일)  *RS / *RSmsg XML 응답 DTO
├── interfaces/             (7 파일, 555 LOC)
│   ├── controller/internals/  (6 파일) ★ Triple 예약시스템이 호출하는 내부 REST API
│   └── request/               (1 파일) TwayAncillaryDeepLinkRequest
└── support/                (22 파일, 771 LOC)
    ├── enums/              (10 파일)   ActionType, SsrType, CommissionType …
    └── model/              (12 파일)   Booking, Passenger, Pricing, Refund, Schedule …

코드를 읽는 순서 (신입용)

  1. interfaces/controller — 외부에 뭘 노출하는지(=무엇을 할 수 있는지) 먼저 본다.
  2. application — 컨트롤러가 호출하는 서비스의 오케스트레이션 흐름.
  3. infrastructure/TwayClient — 실제 HTTP 호출과 SEED 인증.
  4. infrastructure/request·response — 200여 개 DTO는 통째로 외우지 말고, 필요한 오퍼레이션 1개의 *RQmsg/*RSmsg 쌍만 따라가면 된다. (*msg가 루트 봉투, *RQ/*RS가 본문)

왜 infrastructure가 이렇게 거대한가?

T’way XML 스키마는 오퍼레이션마다 별도 루트 메시지(AirAvailabilityRQmsg, CreateBookingRQmsg, ModifyBookingRSmsg …)를 쓰고, 그 안에 좌석/수하물/운임/세금/탑승객 등 중첩 구조를 전부 별도 Kotlin 클래스로 1:1 매핑해 두었다. 로직은 단순하지만 DTO 매핑 부피가 큰 것이 LCC/REST 모듈의 전형이다. 비즈니스 로직을 찾을 때 request/response 패키지를 다 뒤지지 말고 TwayClientapplication만 봐야 한다.


3. 핵심 파일 표

파일 (상대경로)역할별점
infrastructure/TwayClient.kt모든 외부 API 호출의 단일 진입점. SEED 인증, XML 직렬화, 에러 판정(checkError), @Retryable 취소 재시도, 응답→도메인 매핑(toBooking/toItineraries 등)이 전부 여기 모여 있다 (1,089 LOC)★★★
interfaces/controller/internals/TwaySearchController.kt검색 내부 API. 유일하게 @CircuitBreaker(name="twaySearch") 가 걸려 있고, OPEN 시 빈 리스트 fallback + Datadog 스팬 태깅★★★
interfaces/controller/internals/TwayBookingController.kt예약/조회/취소/분리/repricing 등 가장 많은 엔드포인트 보유★★★
interfaces/controller/internals/TwayTicketingController.kt발권 + 재발행은 Redis 폴링 비동기(polling/poller, 202 ACCEPTED → key 폴링)★★★
application/TwayBookingService.kt예약 오케스트레이션: 운임확정→좌석마킹→예약생성→실패시 취소+미노출운임 저장★★
application/TwayFlightSearchService.kt검색: 노선필터 → 코루틴 병렬검색(pmap) → 캐시 → 왕복합성 → 미노출운임 제외★★
application/TwayRouteService.kt취항노선 화이트리스트(Redis 캐시) — LCC 단락 로직의 핵심★★
application/TwayCancelService.ktvoid(취소/항공권미발행) vs refund(환불) 분기 + 체크인 후 취소 차단★★
infrastructure/TwayPaymentError.kt결제 응답 에러코드를 사용자용 ErrorMessage로 변환(발권/부가 결제 실패 분기)★★
infrastructure/request/CardInfo.kt카드 전 항목 SEED 암호화(오프라인 인증 결제)★★
infrastructure/ancillary/TwayAncillaryClient.kt부가서비스 딥링크 토큰 발급(별도 JSON 엔드포인트, application/json)★★
configuration/RedisConfiguration.ktFareItineraryGzip 압축 + JSON으로 Redis에 저장하는 전용 RedisTemplate
support/model/Booking.kt어댑터 공통 도메인으로 변환하기 위한 T’way 내부 모델

4. 공개 인터페이스 — 컨트롤러 엔드포인트 & 서비스

모든 컨트롤러는 interfaces/controller/internals 패키지의 @RestController이며, 베이스 경로는 /internals/TWAY/...다. 중앙 디스패처 없이 컨트롤러 자체가 Triple 예약 시스템의 내부 API로 노출된다(→ system-architecture, request-flow).

4-1. 컨트롤러 → 엔드포인트 맵

컨트롤러HTTP 메서드 + 경로함수오퍼레이션
TwaySearchController /internals/TWAY/searchPOST /searchSearch (★ @CircuitBreaker)
GET /detailSearch (상세)
POST /reissuereissueSearchSearch (재발행)
GET /reissuereissueDetailSearch (재발행 상세)
TwayBookingController /internals/TWAY/bookingsPOST /createBooking
PUT /{pnr}changeApisBooking (APIS/여권 변경)
GET /{pnr}retrieveBooking 조회
PUT /{pnr}/cancelcancelBooking 취소
GET /{pnr}/expected-cancelexpectedCancel예상 취소(환불수수료)
GET /{pnr}/cancelablecancelable취소가능 유형(void/refund)
POST /{pnr}/dividedividePNR 분리
GET /{pnr}/check-pnrcheckPnrPNR 존재 확인
GET /{pnr}/confirmconfirm예약 확정 조회
GET /{pnr}/repricingrepricing재가격 산정
TwayTicketingController /internals/TWAY/ticketingPOST /readyreadyTicketing 준비(confirmPrice)
POST /issueTicketing 발권
POST /additionreissueTicketing 재발행(★ Redis 폴링 비동기, 202)
GET /addition/{reissueKey}checkReissue재발행 결과 폴링
TwayFareRuleController /internals/TWAY/fare-rulesGET /getFareRulesFareRule
GET /structuredgetStructuredFareRulesFareRule(구조화)
TwayAncillaryController /internals/TWAY/ancillariesGET /avail/key, GET /avail/pnrsearchAvailAncillary 가용 조회
GET /baggage/key, GET /baggage/pnrsearchBaggageAncillary 수하물 조회
POST /baggagepurchaseBaggageAncillary 수하물 구매
DELETE /releaseAncillariesAncillary 해제
POST /deep-link (@Deprecated)getAncillaryDeepLinkAncillary 딥링크(구)
POST /types/{type}/deep-linkgetAncillaryDeepLinkAncillary 딥링크(seat/bundle/meal/extra-baggage)
TwayAgencyCreditController /internals/TWAY/agency-creditGET /getAgencyCreditAgencyCredit 잔액 조회

컨트롤러 ↔ 오퍼레이션 매핑이 1:1이 아니다

6개 컨트롤러가 6개 오퍼레이션 카테고리(Search/Booking/Ticketing/FareRule/Ancillary/AgencyCredit)에 거의 대응하지만, 재발행(reissue) 처럼 한 사용자 액션이 Search·Ticketing 두 컨트롤러에 걸쳐 있고, 취소(cancel) 는 Booking 컨트롤러에 들어 있다. 카테고리 표를 그대로 컨트롤러로 믿지 말고 위 표를 기준으로 보라.

4-2. application 서비스 목록과 책임

서비스책임주 의존
TwayFlightSearchService검색·재발행검색·상세, 노선필터+코루틴병렬+캐시+왕복합성+미노출운임제외TwayClient, TwayRouteService, Redis repo 3종
TwayRouteService취항노선 화이트리스트 조회/캐싱(Redis)TwayClient, TwayRouteRepository
TwayBookingService예약 생성(운임확정→좌석마킹→생성), 조회/취소취합/분리, 숨은경유 보강TwayClient, TwayPricingService
TwayPricingServiceFareQuote 운임 확정TwayClient
TwayPassengerServiceAPIS(여권/탑승객) 정보 변경TwayClient
TwayTicketingService발권, 재발행(retrieve→markSeat→confirmPrice→reissue), 실패시 비동기 취소+슬랙TwayClient, SlackService
TwayCancelServicevoid/refund 분기, 체크인 후 취소 차단TwayClient
TwayFareRuleService운임규정 조회TwayClient
TwayAncillaryService수하물/좌석 가용·구매·해제, 딥링크TwayClient, TwayAncillaryClient
TwayAgencyCreditService대리점 크레딧 잔액TwayClient

모든 서비스가 TwayClient 하나에 의존한다

위 표에서 보듯 application 계층의 거의 모든 서비스가 단일 TwayClient로 외부 호출을 위임한다. 즉 T’way 연동의 진짜 본체는 TwayClient.kt 1,089줄이다. 오퍼레이션별 상세 흐름은 tway-operations, XML/SEED 프로토콜 디테일은 tway-protocol를 참고.


5. 중요도 별점

모듈 중요도: ★★ (보통~중요)

근거

  • 운영 비중(↑): 국내 출발 국제선에서 T’way는 실판매 LCC라 트래픽이 적지 않다. 검색 컨트롤러에만 서킷브레이커가 별도로 걸려 있는 것은 장애 격리가 중요한 활성 모듈임을 시사한다(TwaySearchController.kt:27 @CircuitBreaker(name="twaySearch")).
  • 연동 복잡도(↓): GDS 같은 stateful 세션이 없어 호출 패턴이 단순하다. 모든 로직이 TwayClient 한 파일에 집중되어 추적이 쉽다.
  • 고유 위험요소(↑): SEED 외부 jar 의존(TwaySEED.jar), 결제 비밀번호/카드정보 암호화, 재발행 Redis 폴링 비동기, 취소 @Retryable, 미노출운임 저장 등 운영 사고로 직결되는 디테일이 많다. 코드 규모(약 1만 LOC)는 크지만 대부분 DTO라 학습 난이도 자체는 GDS보다 낮다.

→ 종합하면 “이해 난도는 중간, 운영 민감도는 높음”. 신입이 두 번째~세 번째로 학습하기 좋은 LCC 표본이다(가장 단순한 Jeju Air보다 한 단계 풍부).

별점 요약:

관점별점근거
학습 우선순위★★LCC/REST 표준형, GDS 대비 단순
운영 민감도★★★결제·SEED·재발행 비동기·취소 재시도
코드 복잡도★★로직은 단순하나 DTO 200+개
고유 패턴 학습가치★★★SEED 인증, 노선 화이트리스트, Redis 폴링 발권

더 보기