Jin Air — 개요

module-jinair arch-supplier-module api-rest

한 줄 요약

Jin Air(진에어, 항공사 코드 LJ)는 대한항공 계열 LCC(저비용항공사)다. 코드상 분류는 LCC/REST지만, 실제 전문은 SOAP/XML을 JSON 봉투(bodyXml)로 감싸 전송하는 하이브리드 구조이며, 항공권 발권·재발행은 카드 결제까지 동시에 처리된다. 부가서비스(수하물/좌석)와 대리점 크레딧(Agency Credit) 조회를 지원하는 것이 다른 LCC와 구별되는 특징이다.

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


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

1-1. LCC인가? 코드로 검증

Supplier.JINAIR로 식별되며(JinairSearchController.search 35행 request.isSearchable(Supplier.JINAIR)), 단일 항공사 직접 연동(GDS·NDC가 아님)이다. GDS/SOAP 공급사(Amadeus·Sabre·Galileo)와 달리 세션/PNR 상태를 들고 다니는 인증 컨텍스트가 없고, 매 호출마다 x-api-key 헤더 + agencyCode(officeId)로 인증한다.

// JinairClient.kt:98-103 — 모든 호출이 동일한 stateless 인증 패턴
.header(
    mapOf(
        HttpHeaders.CONTENT_TYPE to MediaType.APPLICATION_JSON_VALUE,
        "x-api-key" to apiProperties.key
    )
)

LCC지만 "순수 REST"는 아니다 — 가장 먼저 알아야 할 사실

엔드포인트는 REST 스타일(/availability, /price, /reservation, /ancillary, /seatmap, /information)이고 Content-Typeapplication/json이다. 그러나 본문은 bodyXml 필드 안에 SOAP/XML 페이로드를 담은 JSON 봉투다. JinairXmlBodyRequest(infrastructure/request/JinairRequest.kt:12-33)의 body@SoapBody가 붙어 있고, 각 요청 타입은 SOAP 오퍼레이션명(getAirAvailability, confirmPrice, saveModifyBooking 등)으로 매핑된다. 즉 “REST 위에 얹힌 SOAP” 구조다. 자세한 봉투 구조는 jinair-protocol에서 다룬다.

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

특성내용근거
항공사Jin Air (LJ), 대한항공 계열 LCCSupplier.JINAIR
연동 방식항공사 직접 API (SOAP-over-JSON)JinairRequest.kt, JinairResponse.kt
인증x-api-key + agencyCode(officeId), 무상태JinairClient 전 메서드
결제발권 시 카드 결제 동시 처리 (KeyInCard)JinairTicketingService.issue 39행
부가서비스수하물(baggage)·좌석맵(seatmap)JinairAncillaryService, JinairClient.searchSeatmap
대리점 크레딧잔액 조회 지원JinairAgencyCreditService.getAgencyCredit

당일/익일 출발편은 검색 자체가 차단된다

JinairSearchController.search 30~33행에 핵심 비즈니스 규칙이 박혀 있다.

//진에어는 24시간 이내 출발편 스케쥴 예약시 예약과 결제가 동시에 처리되야하므로 당일/익일 검색은 불가 처리
if (request.departureDate < today().plusDays(2)) {
    return emptyList()
}

Jin Air는 예약(booking)과 결제(payment)가 발권 단계에서 한 번에 일어나므로, 임박한 출발편은 타임리밋 위반·결제 실패 리스크가 커서 출발 2일 전(today().plusDays(2)) 이내는 검색에서 아예 제외한다. 이는 다른 공급사에 없는 Jin Air 고유 제약이며 jinair-pitfalls의 1순위 지뢰다.

1-3. 왜 카드 정보를 암호화하는가

발권/재발행 시 카드번호·유효기간·CVV·카드소지자명을 평문으로 보내지 않고, 전용 암호화(JinairCipher)로 봉인한다. SaveModifyBookingRQ.kt의 결제 필드들에 @Encrypt(cipher = JinairCipher::class)가 선언되어 있다(229·233·237·241·254·335행).

// SaveModifyBookingRQ.kt:229-243
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "paymentTypeNumber")   // 카드번호
val paymentTypeNumber: String? = null,
@Encrypt(cipher = JinairCipher::class)
val expirationMonth: String? = null,
@Encrypt(cipher = JinairCipher::class)
val expirationYear: String? = null,
@Encrypt(cipher = JinairCipher::class)
val cvv2Number: String? = null,

JinairCipher(support/util/JinairCipher.kt:42-82)는 RSA + AES 하이브리드다.

  1. 40자 난수 nonce 생성(generateNonce).
  2. nonce를 RSA 공개키(X.509 인증서, RSA/ECB/PKCS1Padding)로 암호화 → encryptedNonce.
  3. nonce의 SHA-256 해시 앞 16바이트로 AES 대칭키를 만들어 본문 암호화 → encryptedText.
  4. "$encryptedText$DELIMITER$encryptedNonce" 형태로 결합해 전송.

흔한 오해: "Jin Air는 SEED 암호화" → 틀림

Jin Air의 암호화는 RSA+AES다. SEED(TwaySEED.jar) 암호화는 T’way Air 모듈의 특징이고 Jin Air와 무관하다. JinairCipher.decrypt@Deprecated로 예외를 던지며(67~70행) 복호화는 지원하지 않는다(단방향 발신 전용). 이 혼동은 신입이 자주 빠지는 함정이므로 jinair-pitfalls에 기록되어 있다.


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

전체 11개 공급사 중 파일 114개 / 약 9,249 LOC5번째 규모다(Sabre·Galileo·Amadeus·AmadeusNDC 다음, Tway와 비슷한 중간 크기).

supplier/jinair/
├── application/                 (8개 서비스 — 유스케이스 오케스트레이션)
│   ├── JinairFlightSearchService.kt
│   ├── JinairBookingService.kt
│   ├── JinairTicketingService.kt
│   ├── JinairCancelService.kt
│   ├── JinairPassengerService.kt
│   ├── JinairAncillaryService.kt
│   ├── JinairFareRuleService.kt
│   └── JinairAgencyCreditService.kt
├── configuration/
│   └── RedisConfiguration.kt    (FareItinerary 전용 Redis 템플릿, gzip 직렬화)
├── domain/
│   ├── model/FareItinerary.kt   (캐시되는 핵심 도메인 모델)
│   └── repository/JinairFareItineraryRepository.kt  (Redis Hash 저장소)
├── infrastructure/
│   ├── JinairClient.kt          (★ 외부 API 단일 진입점, ~1,240행)
│   ├── request/  (25개 RQ DTO + JinairRequest 봉투)
│   └── response/ (50개 RS DTO + JinairResponse 봉투/ErrorType)
├── interfaces/
│   └── controller/internals/    (6개 REST 컨트롤러)
└── support/
    ├── enums/   (CardType, CommissionType, PassengerTypeCode, PaymentError, TaxCode)
    ├── model/   (14개 — Booking, Passenger, Pricing, Refund, Schedule, Fare 등)
    └── util/    (AirportUtils, JinairCipher)

레이어 읽는 순서 (신입용)

interfaces(컨트롤러) → application(서비스) → infrastructure(JinairClient) → support/domain(모델). 모든 외부 HTTP 호출은 JinairClient 한 곳에 모여 있으니, 동작을 파악할 때는 컨트롤러에서 시작해 서비스를 거쳐 JinairClient의 같은 이름 메서드로 내려가면 된다. request-flow의 공통 흐름과 정확히 일치한다.

핵심 파일 표

파일역할비고
infrastructure/JinairClient.kt외부 API 단일 클라이언트(검색·요금·예약·발권·취소·재발행·부가·크레딧)ClientSupport 상속, searchTimeout=15s/defaultTimeout=40s
application/JinairFlightSearchService.kt검색/요금/재발행검색 오케스트레이션, 캐시·코루틴 병렬pmap+cartesianProduct로 OD 병렬 호출
application/JinairBookingService.kt예약 생성(book)·재예약 검증·PNR 분리(divide)·repricing좌석 마킹→예약 2단계
application/JinairTicketingService.kt발권(issue)·발권준비(ready)·재발권(reissue)실패 시 비동기 자동취소(cancelAsync)
application/JinairCancelService.kt취소 예상/가능여부/실행checkIfCancelable 사전검증
application/JinairAncillaryService.kt수하물/부가서비스 가용 조회(key·pnr 기준)
application/JinairFareRuleService.kt운임 규정 조회 + Redis 캐시data class 서비스(특이)
application/JinairAgencyCreditService.kt대리점 크레딧 잔액 조회가장 단순한 서비스
infrastructure/request/JinairRequest.ktSOAP-over-JSON 요청 봉투(bodyXml, service 매핑)JinairXmlBodyRequest/JinairJsonBodyRequest
infrastructure/response/JinairResponse.kt응답 봉투 + checkError/isTimeoutError 공통 로직errorType=="Error" 처리
support/util/JinairCipher.kt카드 결제 정보 RSA+AES 하이브리드 암호화JinairSecureKey가 인증서 공개키 로드
domain/repository/JinairFareItineraryRepository.kt검색 결과 FareItinerary Redis Hash 캐시CacheSet.FARE_ITINERARY
configuration/RedisConfiguration.ktFareItinerary 전용 RedisTemplate(gzip 직렬화)JinairRedisConfiguration

3. 공개 인터페이스 (Triple 예약 시스템이 호출하는 내부 API)

중앙 디스패처 없이 공급사별 컨트롤러가 직접 노출된다(베이스 경로 /internals/JINAIR/...). 6개 컨트롤러가 8개 서비스를 사용한다.

flowchart TD
    Triple["Triple 예약 시스템 (내부 호출자)"]
    Triple -->|"HTTP"| SearchC
    Triple -->|"HTTP"| BookingC
    Triple -->|"HTTP"| TicketingC
    Triple -->|"HTTP"| FareRuleC
    Triple -->|"HTTP"| AncillaryC
    Triple -->|"HTTP"| AgencyCreditC

    subgraph "interfaces — 6개 컨트롤러"
        SearchC["JinairSearchController"]
        BookingC["JinairBookingController"]
        TicketingC["JinairTicketingController"]
        FareRuleC["JinairFareRuleController"]
        AncillaryC["JinairAncillaryController"]
        AgencyCreditC["JinairAgencyCreditController"]
    end

    subgraph "application — 8개 서비스"
        SearchS["JinairFlightSearchService"]
        BookingS["JinairBookingService / CancelService / PassengerService"]
        TicketingS["JinairTicketingService"]
        FareRuleS["JinairFareRuleService"]
        AncillaryS["JinairAncillaryService"]
        AgencyCreditS["JinairAgencyCreditService"]
    end

    SearchC --> SearchS
    BookingC --> BookingS
    TicketingC --> TicketingS
    FareRuleC --> FareRuleS
    AncillaryC --> AncillaryS
    AgencyCreditC --> AgencyCreditS

    SearchS --> Client
    BookingS --> Client
    TicketingS --> Client
    FareRuleS --> Client
    AncillaryS --> Client
    AgencyCreditS --> Client

    Client["JinairClient (외부 API 단일 진입점)"]
    Client --> JinairAPI["Jin Air API (SOAP-over-JSON)"]
    Client --> Redis["Redis (FareItinerary, ReissueResult polling)"]

3-1. 컨트롤러 & 엔드포인트

컨트롤러베이스 경로엔드포인트 (@*Mapping)용도
JinairSearchController/internals/JINAIR/searchPOST /, GET /, POST /reissue, GET /reissue검색 / 상세 / 재발행검색 / 재발행상세
JinairBookingController/internals/JINAIR/bookingsPOST /, GET /{pnr}, GET /{pnr}/confirm, PUT /{pnr}, GET /{pnr}/expected-cancel, GET /{pnr}/cancelable, PUT /{pnr}/cancel, POST /{pnr}/divide, GET /{pnr}/repricing예약/조회/APIS변경/취소/분리/재가격
JinairTicketingController/internals/JINAIR/ticketingPOST /ready, POST /, POST /addition, GET /addition/{reissueKey}발권준비/발권/재발권/재발권폴링
JinairFareRuleController/internals/JINAIR/fare-rulesGET /, GET /structured운임규정/구조화규정
JinairAncillaryController/internals/JINAIR/ancillaryGET /avail/key, GET /avail/pnr, GET /baggage/key, GET /baggage/pnr부가서비스/수하물 가용 조회
JinairAgencyCreditController/internals/JINAIR/agency-creditGET /대리점 크레딧 잔액

재발권만 비동기(Deferred) 패턴

JinairTicketingController.reissue(55행)는 Redis 기반 polling(...)으로 작업을 비동기 시작하고 202 ACCEPTED + pollingKey를 즉시 반환한다. 호출자는 GET /addition/{reissueKey}poller<ReissueResult<...>>를 폴링해 PENDING/ERROR/COMPLETE 상태를 받는다. 발권/재발권은 외부 결제까지 동반해 오래 걸리므로 동기 타임아웃을 피하려는 설계다. 비동기/코루틴 메커니즘은 async-coroutines 참고.

3-2. application 서비스 ↔ 지원 오퍼레이션 매핑

요구된 6개 오퍼레이션이 모두 구현되어 있다.

오퍼레이션서비스진입 컨트롤러 메서드JinairClient 메서드
SearchJinairFlightSearchServicesearch/detail/reissueSearch/reissueDetailsearch, reissueSearch, inboundReissueSearch, doPricing
BookingJinairBookingService, JinairCancelService, JinairPassengerServicecreate/retrieve/changeApis/cancel/divide/repricingmarkSeat, createBooking, confirmPrice, retrieve, getCancelInfo, cancelBooking, changeApis, divide
TicketingJinairTicketingServiceready/issue/reissue/checkReissueconfirmPrice, issue, reissueConfirmPrice, reissue
FareRuleJinairFareRuleServicegetFareRules/getStructuredFareRulesgetFareRule
AncillaryJinairAncillaryServicesearchAncillary/searchBaggagesearchBaggageAvail, searchBaggage, searchSeatmap
AgencyCreditJinairAgencyCreditServicegetAgencyCreditgetAgencyCredit

4. 중요도 별점

중요도: ★★ (중)

평가근거
코드 규모★★114파일/9.2K LOC, 11개 중 5위 (GDS 3사보다 작고 LCC 중 큼)
연동 복잡도★★★SOAP-over-JSON 봉투 + RSA/AES 암호화 + 발권 시 결제 동시처리 + 비동기 재발권
비즈니스 영향★★한국 LCC 수요(특히 단거리 노선) 커버, 부가서비스/대리점 크레딧 매출 연관
함정 밀도★★★당일/익일 검색 차단, 결제 실패 자동취소, SEED 오해, 단방향 암호화 등 jinair-pitfalls 다수

종합 ★★ — 규모는 중간이지만 “예약=결제” 모델과 SOAP/암호화 하이브리드 때문에 연동 디테일·운영 함정 면에서는 GDS 못지않게 까다롭다. 온보딩 시 LCC라고 가볍게 보지 말 것.


다음에 읽을 노트

  • jinair-operations — 6개 오퍼레이션의 단계별 호출 시퀀스(검색→예약→발권→재발권)
  • jinair-protocolbodyXml SOAP-over-JSON 봉투, service 매핑, RSA+AES 암호화 상세
  • jinair-pitfalls — 당일/익일 검색 차단, 발권 실패 자동취소, SEED 오해 등 지뢰
  • system-architecture / request-flow — 전체 어댑터 아키텍처와 공통 요청 흐름