Jin Air(진에어, 항공사 코드 LJ)는 대한항공 계열 LCC(저비용항공사)다. 코드상 분류는 LCC/REST지만, 실제 전문은 SOAP/XML을 JSON 봉투(bodyXml)로 감싸 전송하는 하이브리드 구조이며, 항공권 발권·재발행은 카드 결제까지 동시에 처리된다. 부가서비스(수하물/좌석)와 대리점 크레딧(Agency Credit) 조회를 지원하는 것이 다른 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-Type은 application/json이다. 그러나 본문은 bodyXml 필드 안에 SOAP/XML 페이로드를 담은 JSON 봉투다. JinairXmlBodyRequest(infrastructure/request/JinairRequest.kt:12-33)의 body에 @SoapBody가 붙어 있고, 각 요청 타입은 SOAP 오퍼레이션명(getAirAvailability, confirmPrice, saveModifyBooking 등)으로 매핑된다. 즉 “REST 위에 얹힌 SOAP” 구조다. 자세한 봉투 구조는 jinair-protocol에서 다룬다.
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행).
nonce의 SHA-256 해시 앞 16바이트로 AES 대칭키를 만들어 본문 암호화 → encryptedText.
"$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 LOC로 5번째 규모다(Sabre·Galileo·Amadeus·AmadeusNDC 다음, Tway와 비슷한 중간 크기).
interfaces(컨트롤러) → application(서비스) → infrastructure(JinairClient) → support/domain(모델). 모든 외부 HTTP 호출은 JinairClient 한 곳에 모여 있으니, 동작을 파악할 때는 컨트롤러에서 시작해 서비스를 거쳐 JinairClient의 같은 이름 메서드로 내려가면 된다. request-flow의 공통 흐름과 정확히 일치한다.
JinairTicketingController.reissue(55행)는 Redis 기반 polling(...)으로 작업을 비동기 시작하고 202 ACCEPTED + pollingKey를 즉시 반환한다. 호출자는 GET /addition/{reissueKey}로 poller<ReissueResult<...>>를 폴링해 PENDING/ERROR/COMPLETE 상태를 받는다. 발권/재발권은 외부 결제까지 동반해 오래 걸리므로 동기 타임아웃을 피하려는 설계다. 비동기/코루틴 메커니즘은 async-coroutines 참고.