Jin Air — 프로토콜·전문

module-jinair pattern-soap-in-json api-rest config-and-infra

한 줄 요약

Jin Air(LJ)는 “LCC/REST”로 분류되지만, 실제 전송 계층은 IBS Software iRes(SI:http://www.ibsplc.com/iRes/simpleTypes/)의 SOAP 메시지를 JSON 봉투(bodyXml) 안에 문자열로 감싸 HTTP POST하는 독특한 하이브리드 구조다. GDS처럼 stateful 세션은 없고(토큰 발급 단계 없음), x-api-key 헤더 + 매 요청 AgencyCode 로 인증한다. FareRule·Seatmap만 SOAP 없이 순수 REST JSON을 쓴다.

관련 노트: jinair-operations · jinair-pitfalls · interfaces-dtos · jinair-overview


1. 프로토콜 전체 그림 — “SOAP-in-JSON” 게이트웨이

Jin Air의 가장 중요한 특징은 단일 REST 엔드포인트가 아니라 두 가지 직렬화 방식이 한 어댑터 안에 공존한다는 점이다. 둘을 가르는 기준은 요청 래퍼 클래스(JinairXmlBodyRequest vs JinairJsonBodyRequest)이며, 이는 infrastructure/request/JinairRequest.kt에 정의돼 있다.

// JinairRequest.kt:6-10
private interface JinairRequest<T> {
    val officeId: String   // = AgencyCode
    val service: String    // iRes 오퍼레이션명 (예: "getAirAvailability")
    val body: T?
}

전송되는 바깥 봉투는 항상 JSON이다. 차이는 body 필드(@JsonProperty("bodyXml"))가 어떻게 직렬화되느냐다.

래퍼 클래스body 직렬화적용 오퍼레이션정의 위치
JinairXmlBodyRequest<T>@SoapBodySOAP Envelope XML 문자열검색/예약/발권/취소/조회/분리/수하물/대리점크레딧JinairRequest.kt:12-33
JinairJsonBodyRequest<T>@JsonBody(요청은 일반 JSON)FareRule, SeatmapJinairRequest.kt:35-45
flowchart TD
    A["Adapter"] -->|"POST endpoint/availability"| G["iRes 진에어 게이트웨이"]
    A --> H["요청 헤더<br/>Content-Type application/json<br/>x-api-key ****"]
    A --> B["요청 바디 JSON 봉투<br/>officeId AgencyCode<br/>service getAirAvailability<br/>bodyXml SOAP Envelope XML 문자열"]
    B --> X["bodyXml 안에 SOAP-ENV Envelope / Body가 들어가고<br/>그 안에 sim AirAvailabilityRQ<br/>xmlns sim http www.ibsplc.com iRes simpleTypes<br/>XML이 통째로 문자열로 인코딩됨"]
  • 바깥 봉투는 항상 JSON, bodyXml 필드 값으로 SOAP Envelope XML 전체가 이스케이프된 문자열로 들어감.
  • 헤더: Content-Type: application/json, x-api-key: ****.
  • SOAP 본문 루트: sim:AirAvailabilityRQ (xmlns:sim="http://www.ibsplc.com/iRes/simpleTypes/").

service 값은 body 타입으로 자동 결정된다

JinairXmlBodyRequest.servicewhen (body) { is AirAvailabilityRQ -> "getAirAvailability" ... } 의 분기로 산출된다(JinairRequest.kt:18-32). 즉 요청 DTO 타입 하나가 iRes 오퍼레이션명·엔드포인트·직렬화 방식을 모두 결정한다. 매핑이 안 되는 타입은 IllegalArgumentException("Unsupported request body type").

service ↔ 엔드포인트 ↔ 어댑터 메서드 대응표

JinairClient.kt의 각 메서드는 "${endpoint}/{path}"로 POST한다. (모든 SOAP 계열은 Content-Type: application/json + x-api-key)

어댑터 메서드HTTP 경로service요청/응답 DTO직렬화
search/availabilitygetAirAvailabilityAirAvailabilityRQ / AirAvailabilityRSSOAP-in-JSON
reissueSearch/availabilitygetEnhancedAirAvailabilityEnhancedAirAvailabilityRQ / EnhancedAirAvailabilityRSSOAP-in-JSON
doPricing/priceconfirmPriceConfirmPriceRQ / ConfirmPriceRSSOAP-in-JSON
markSeat/reservationadjustFlightInventoryAdjustFlightInventoryRQ / AdjustFlightInventoryRSSOAP-in-JSON
createBooking/reservationsaveCreateBookingCreateBookingRQ / CreateBookingRSSOAP-in-JSON
confirmPrice(예약 후 운임확정) / reissueConfirmPrice/reservationmodifyBookingModifyBookingRQ / ModifyBookingRSSOAP-in-JSON
issue / cancelBooking / changeApis / reissue/reservationsaveModifyBookingSaveModifyBookingRQ / SaveModifyBookingRSSOAP-in-JSON
retrieve/reservationretrieveBookingRetrieveBookingRQ / RetrieveBookingRSSOAP-in-JSON
getCancelInfo/reservationcancelBookingCancelBookingRQ / CancelBookingRSSOAP-in-JSON
divide/reservationsplitReservationSplitPnrRQ / SplitPnrRSSOAP-in-JSON
searchBaggageAvail / searchBaggage/ancillarylistBaggageServicesListBaggageServicesRQ / ListBaggageServicesRSSOAP-in-JSON
getAgencyCredit/reservationretrieveAgencyCreditRetrieveAgencyCreditRQ / RetrieveAgencyCreditRSSOAP-in-JSON
getFareRule/informationfareRegulationDataList<FareRuleRQ> / List<FareRuleRS>순수 JSON
searchSeatmap/seatmapshowSeatMapShowSeatmapRQ / ShowSeatmapRS순수 JSON
registerDsr{dsr.endpoint}/ota/dsrMake(없음)form 파라미터form-urlencoded
getTicketingErrorMessage{payment.endpoint}/rest/trans/authFailRs(없음)GET 쿼리 logUidREST GET

/reservation 경로 하나에 6개 오퍼레이션이 몰려 있다

createBooking·confirmPrice·issue·retrieve·getCancelInfo·cancelBooking·changeApis·divide·reissue·getAgencyCredit가 모두 /reservation을 친다. 어떤 동작인지는 경로가 아니라 service 값(=body 타입) 으로만 구분된다. 디버깅 시 경로만 보고 오퍼레이션을 추정하면 안 된다. → jinair-pitfalls


2. SOAP 봉투 직렬화 메커니즘 (support/util/SoapExtensions.kt)

bodyXml 필드에 붙은 @SoapBody(SoapExtensions.kt:192-197)가 Jackson 커스텀 (역)직렬화기를 트리거한다.

직렬화(요청 보낼 때, SoapBodySerializer, SoapExtensions.kt:215-232)

  1. 스프링 컨텍스트에서 xmlMapper 빈을 꺼낸다(BeanUtils.getBean("xmlMapper")).
  2. 요청 DTO를 xmlMapper로 XML 바이트로 변환(INDENT_OUTPUT 비활성 → 한 줄).
  3. soap { body(...) } 헬퍼로 그 XML을 SOAP Envelope/Body 안에 끼워넣어 완성된 SOAP 문자열을 만든다.
  4. 그 문자열 전체를 JSON 필드 값으로 gen.writeString(...) 한다.

역직렬화(응답 받을 때, SoapBodyDeserializer, SoapExtensions.kt:199-213)

  1. bodyXml JSON 문자열을 다시 SOAP 메시지로 파싱.
  2. soapBody.firstChild(= 실제 *RS 루트 엘리먼트)만 떼어내 xmlMapper.readValue(..., genericType)로 응답 DTO로 매핑.
// SoapBodyDeserializer.deserialize  (SoapExtensions.kt:206-212)
val soapMessage = MessageFactory.newInstance()
    .createMessage(MimeHeaders(), parser.valueAsString.byteInputStream())
return xmlMapper.readValue(soapMessage.soapBody.firstChild.asString().byteInputStream(), genericType)

왜 SOAP을 직접 안 쓰고 JSON으로 감쌌나

응답 봉투(JinairXmlBodyResponse, JinairResponse.kt:40-51)에는 errorMessage/errorType/stackTrace/officeId/service/CLIENT_SESSION_ID 같은 게이트웨이 메타데이터가 JSON으로 함께 온다. 즉 진에어 측이 IBS iRes의 SOAP API 앞에 JSON 게이트웨이를 한 겹 올려둔 형태이며, 어댑터는 이 게이트웨이 스펙(officeId/service/bodyXml)에 맞춘 것이다. 어댑터가 직접 SOAP 엔드포인트를 호출하는 것이 아니다.

응답 봉투(JinairResponse) 공통 필드

infrastructure/response/JinairResponse.kt

필드JSON 키의미
errorMessageerrorMessage게이트웨이 레벨 오류 메시지("timeout" 포함 시 타임아웃 판정)
errorTypeerrorType"Error"면 게이트웨이 레벨 실패
stackTracestackTrace게이트웨이 스택트레이스
officeId / service동일에코백된 AgencyCode·오퍼레이션명
clientSessionIdCLIENT_SESSION_ID결제 실패 메시지 재조회 키 (대문자 키 주의)
bodybodyXml@SoapBody/@JsonBody로 디코딩된 실제 응답 DTO
// JinairResponse.isTimeoutError (JinairResponse.kt:19-20)
val isTimeoutError: Boolean
    get() = errorMessage?.lowercase()?.contains("timeout") ?: false

2단계 오류 체크 구조

오류는 봉투 레벨(errorType == "Error")과 body 레벨(AirAvailabilityRS.errorType: ErrorType, ConfirmPriceRS.errorType 등) 두 군데에서 난다. JinairResponse.checkError(bodyErrorType, callback)(JinairResponse.kt:22-37)가 먼저 봉투 errorType=="Error"를 검사(단 "Please check departure/arrival airport code"는 무시)한 뒤, body의 ErrorType{errorCode, errorValue}를 콜백으로 넘긴다. 오류코드 해석은 jinair-operations·jinair-pitfalls 참조.


3. 인증·세션 모델

stateful 세션 없음 (GDS와 결정적 차이)

Amadeus/Sabre/Galileo 같은 GDS와 달리 진에어는 로그인/토큰 발급/세션 시작-종료 단계가 없다. 매 요청이 독립적이며, 인증은 다음 둘로 끝난다.

인증 요소위치비고
x-api-key 헤더모든 SOAP/JSON 요청의 .header(...)값 = apiProperties.key (JinairApiProperties.key)
AgencyCode / officeId봉투 officeId + 각 RQ 본문 AgencyCode값 = apiProperties.agencyCode. funnel별로 다른 대리점 코드
DSR API_KEY 헤더registerDsr별도 엔드포인트(dsr.endpoint)·별도 키(dsr.key)
결제 키 (인증서)getTicketingErrorMessagepayment.endpoint/payment.key(X.509 인증서 파일명)

채널/퍼널 분기는 MDC 기반이다.

// Properties.kt:229-239  JinairProperties.getApiProperties
fun getApiProperties(
    salesChannel: String = MDCHolder.SalesChannel.get(),
    salesFunnel: String = MDCHolder.SalesFunnel.get(),
): JinairApiProperties =
    channels.find { it.channel == salesChannel }?.get(funnel = salesFunnel)
        ?: throw InternationalAdapterException(NOT_SUPPORTED_SALES_CHANNEL, salesChannel)
// Properties.kt:408-419  funnel별 설정 단위
data class JinairApiProperties(
    override val funnel: String,
    val endpoint: String,
    val agencyCode: String,
    val key: String,                       // x-api-key
    val payment: JinairPaymentProperties,  // endpoint, key(X.509 cert 파일명)
    val dsr: JinairDsrProperties,          // endpoint, key
) : FunnelProperties

설정은 application.ymlclasspath:supplier/jinair.yml을 import하며, prefix는 supplier.jinair(@ConfigurationProperties(prefix = "supplier.jinair"), Properties.kt:193). 자세한 구성은 configuration-and-infra 참조.

결제 카드 암호화 — JinairCipher (RSA+AES 하이브리드)

발권/재발권 시 카드 정보는 평문으로 보내지 않는다. GuestPaymentInfo(SaveModifyBookingRQ 내부)의 카드 필드에 @Encrypt(cipher = JinairCipher::class)가 붙어 직렬화 직전에 암호화된다.

// SaveModifyBookingRQ.kt (GuestPaymentInfo)
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "paymentTypeNumber")  val paymentTypeNumber: String?   // 카드번호
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "ExpirationMonth")    val expirationMonth: String?
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "ExpirationYear")     val expirationYear: String?
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "cvv2Number")         val cvv2Number: String?          // 항상 "999" 고정
@Encrypt(cipher = JinairCipher::class)
@JacksonXmlProperty(localName = "CardHolderName")     val cardHolderName: String?

암호화 알고리즘(supplier/jinair/support/util/JinairCipher.kt):

flowchart TD
    N["1. nonce 40자 랜덤 0-9a-z<br/>generateNonce"] --> EN["2. encryptedNonce 는 RSA public nonce<br/>ECB PKCS1Padding funnel별 X.509 인증서"]
    N --> SK["3. secretKey 는 SHA-256 nonce 앞 16자<br/>AES-128 key, generateSecretKey"]
    SK --> ET["4. encryptedText 는 AES secretKey 평문"]
    ET --> FIN["5. 최종값 은 encryptedText 더하기 DELIMITER 더하기 encryptedNonce"]
    EN --> FIN
  • 최종값 구분자 DELIMITER 문자열: %~~`%~~~~~~~%^**(%$#%
  • encryptedNonce는 RSA(ECB/PKCS1Padding, funnel별 X.509 인증서), secretKey는 SHA-256(nonce)의 앞 16바이트로 만든 AES-128 키.

JinairSecureKey(@Component)가 부팅 시 classpath:/supplier/jinair/{payment.key} X.509 인증서를 읽어 funnel별 publicKeyMap을 만든다. decrypt는 의도적으로 미지원(@Deprecated, 예외 throw) — 단방향 전송 전용.

cvv2Number는 항상 "999" 하드코딩

GuestPaymentInfo.ofCard에서 cvv2Number = "999"로 고정한다(SaveModifyBookingRQ.kt). 실 CVV를 보내지 않는 진에어 게이트웨이 규약이며, 추가 카드 정보(AdditionalCreditCardInfo)로 보완한다. 결제 실패 시 clientSessionIdgetTicketingErrorMessage를 비동기 호출해 원인 메시지를 로깅한다. → jinair-pitfalls


4. 요청 전문 모델 — 핵심 필드 매핑

4-1. 검색 AirAvailabilityRQ (request/AirAvailabilityRQ.kt)

@JacksonXmlRootElement(localName = "sim:AirAvailabilityRQ") + xmlns:sim = http://www.ibsplc.com/iRes/simpleTypes/

XML 엘리먼트Kotlin 필드값/규칙
AirlineCodeairlineCode(private)고정 "LJ"
FareLevelsfareLevels(private)고정 "IS"
PointOfPurchasepointOfPurchase(private)고정 "KR"
BookingChannelbookingChannelChannelType=API, Channel=OII, Locale=ko_KR (BookingChannel.kt)
AvailabilitySearchesavailabilitySearches[]Origin/Destination/TravelDate(yyyy-MM-dd)
PaxCountDetailspaxCountDetails[]PaxType/PaxCount (count>0만 생성, PaxCountDetail.kt)
TripTypetripTypeOW/RT/MC (origin-dest 개수·왕복 판정)
TravelAgencyCodetravelAgencyCode프로모션 코드 있을 때만 AgencyCode 세팅(AirAvailabilityRQ.kt:79)

@JacksonXmlElementWrapper(useWrapping = false)의 의미

iRes는 <AvailabilitySearches>를 래퍼 없이 반복 엘리먼트로 평탄하게 기대한다. useWrapping=false가 없으면 <availabilitySearches><AvailabilitySearch>...처럼 한 겹 더 감싸져 스키마 위반이 된다. 진에어 RQ/RS DTO 전반에서 이 패턴이 반복된다.

4-2. 운임확정 ConfirmPriceRQ / 좌석마킹 AdjustFlightInventoryRQ

둘 다 sim: 루트 + AirlineCode="LJ". AdjustFlightInventoryRQ의 성공 응답에서 pnrSessionId를 받아(AdjustFlightInventoryRS.pnrSessionId) CreateBookingRQ.PnrSessionId로 넘기는 세션 ID 릴레이가 예약 흐름의 핵심이다(상세 흐름 jinair-operations).

4-3. 예약 CreateBookingRQ (request/CreateBookingRQ.kt)

@JacksonXmlRootElement(localName = "sim:CreateBookingRQ"). 고정 상수: AirlineCode="LJ", PointOfSale="KR", PnrType="NORMAL", pnrOnHoldIndicator=true.

엘리먼트필드비고
AgencyCode/OriginalAgentID/CurrentAgentID모두 agencyCode 동일값
BookerDetailsbookerDetail대표 성인 GivenName/SurName
PnrSessionIdpnrSessionIdmarkSeat 응답에서 받은 세션
PaxCountDetails/ItineraryDetails/FareInfo/GuestDetails/TravelDocuments승객·여정·운임·여권
PnrContactpnrContactCellNumberCountryCode="+82", PrefferedLanguage="Korean" 고정

필드명 오타가 스펙이다

iRes 스펙 자체의 철자 오류를 그대로 따라야 한다: IsPrefferedContact, PrefferedLanguage(Address/PnrContact), RecieptNumber(GuestPaymentInfo). 이를 “고치면” 전문이 매핑되지 않는다. → jinair-pitfalls

4-4. 발권/취소/APIS/재발권 — SaveModifyBookingRQ (다목적, request/SaveModifyBookingRQ.kt)

하나의 DTO를 5개 팩토리로 재사용한다. service는 모두 saveModifyBooking이며, 채워지는 필드 조합으로 동작이 갈린다.

팩토리용도핵심 채움 필드
ofTicketing발권GuestPaymentInfo[](카드/현금)
ofCancel취소isCancelPnr=true
ofApis여권/APIS 변경TravelDocumentChangeType[]
ofReissue재발행GuestPaymentInfo[] + 일정변경

4-5. FareRule FareRuleRQ — 순수 JSON (request/FareRuleRQ.kt)

여기만 @JacksonXmlProperty가 없고 @JsonProperty(camelCase·iRes 약어 키)를 쓴다. → JSON 직렬화 확정.

JSON 키필드
fareTypeCdListfareTypes운임타입 리스트
itnTypCdtripDirectionOW/RT
rbdbookingClass예약클래스
departureAirportCd/arrivalAirportCd공항
departureDtdepartureDateyyyyMMdd
currencyCdcurrency기본 "KRW"
langCdlanguageCode고정 "KOR"

FareRule은 List를 통째로 보낸다

getFareRulerequests.wrapJsonBody(...)List<FareRuleRQ>를 보내고, JinairJsonBodyRequest.servicebody is List<*> && body.firstOrNull() is FareRuleRQ → "fareRegulationData"로 판정한다(JinairRequest.kt:41). 응답도 List<FareRuleRS>(@JsonBody).


5. 응답 전문 모델 — 핵심 매핑

5-1. 검색 AirAvailabilityRS (response/AirAvailabilityRS.kt)

iRes 검색 응답은 OriginDestinationInfo → TripInfo(여정/세그먼트) + PricingInfo(운임) 가 분리된 정규화 구조다. 둘은 인덱스로 연결된다(TripIndex == TripRefIndex, SegmentIndex == SegmentRefIndex, BookingClass 일치).

flowchart TD
    Root["AirAvailabilityRS"] --> Err["ErrorType errorCode, errorValue<br/>body 레벨 오류"]
    Root --> ODI["OriginDestinationInfo 배열<br/>attr Origin Destination PricingUnitID"]
    ODI --> Trip["TripInfo 배열<br/>attr TripIndex, Route"]
    Trip --> Seg["SegmentInfo 배열<br/>attr SegmentIndex, Stops, JourneyTime, DayChange"]
    Seg --> FID["FlightIdentifierInfo<br/>CarrierCode, FlightNumber, FlightSuffix"]
    Seg --> DA["DepartureInfo ArrivalInfo<br/>AirportCode, DateTime, TimeZoneOffset"]
    Seg --> AC["AircraftInfo Type"]
    Seg --> SA["SegmentAvailability 배열<br/>BookingClass, SeatAvailablity, InventoryStatus, CabinClass"]
    ODI --> PI["PricingInfo 배열<br/>attr TripRefIndex"]
    PI --> PPI["PaxPricingInfo 배열<br/>PaxType, AppliedFare Tax Surcharge DisplayFare Discount"]
    PI --> PCI["PricingComponentInfo<br/>FareLevel, FareBasis, FareType, FareId"]
    PI --> SRI["SegmentReferenceInfo 배열<br/>SegmentRefIndex, BookingClass"]
  • TripInfo와 PricingInfo는 인덱스로 연결된다: TripIndex == TripRefIndex, SegmentIndex == SegmentRefIndex, BookingClass 일치.
  • SeatAvailablity는 iRes 스펙의 오타를 그대로 따른 필드명이다.

좌석 매칭은 isAvailable()로 판정

SegmentAvailability.isAvailable(seatCount) = inventoryStatus == "AV" && seatAvailability >= seatCount (AirAvailabilityRS.kt:108-110). 또 SeatAvailablity(오타 그대로), avail = min(9, seatAvailability)로 9석 캡을 건다. 운임 선택은 cabin별 그룹에서 minByOrNull { adultPrice }(JinairClient.kt:182-194).

GUM 노선 무료 수하물 하드코딩

SegmentInfo.toSegment에서 출발/도착 한쪽이 GUM이면 무료 수하물 23kg, 아니면 15kg로 고정(AirAvailabilityRS.kt:195). 스키마가 아니라 코드가 들고 있는 비즈니스 규칙이다.

5-2. 발권/조회 응답 → 결제 매핑 GuestPaymentInfo(response)

응답측 GuestPaymentInfo(response/GuestPaymentInfo.kt)는 PaymentElementType으로 FARE/FEE를 구분하고 toPayment()로 도메인 Payment(승인번호 AuthorisationCode, 승인시각 TransactionTime)로 변환한다. 영국식 철자 AuthorisationCode에 주의.

5-3. FareRule FareRuleRS (response/FareRuleRS.kt)

JSON 응답. iRes 약어 키 → 한국어 운임규정 텍스트로 가공한다.

JSON 키의미
changeData/refundData/noshowData변경/환불/노쇼 안내 문자열 배열
feeItemList위약금 상세(FeeItem: mark/feeAmount/적용 시작·종료일)
advancePurchase+advancePurchaseCd사전구매조건(M=월,D=일) → purchasePeriod
nabiInfo”가이드에 없는 필드” 진에어(나비)포인트 — 코드 주석 명시
resultnull=규정 매칭, "-"=매칭 없음

toFareRules()가 사전발권·변경·취소/환불·노쇼·마일리지·여행일정순위·공통규정을 FareRule로 조립하며, 일부 약관 문구는 코드에서 replace로 제거/치환한다.


6. XSD 스키마 위치와 핵심 타입

원본 IBS iRes XSD가 테스트 리소스에 동봉돼 있다(총 369개 .xsd).

  • 위치: /workspace/nol/nol-triple/air-intl-adapter/src/test/schema/jinair/
  • 공통 타입: SimpleTypes.xsd, CommonTypes.xsd (<xs:include>로 참조)
  • targetNamespace: http://www.ibsplc.com/iRes/simpleTypes/ (= 코드의 sim: 네임스페이스와 일치)

어댑터가 실제 사용하는 오퍼레이션의 RQ/RS XSD:

오퍼레이션XSD 파일
검색AirAvailabilityRQ.xsd / AirAvailabilityRS.xsd
재발행 검색EnhancedAirAvailabilityRQ.xsd / EnhancedAirAvailabilityRS.xsd
운임확정ConfirmPriceRQ.xsd / ConfirmPriceRS.xsd
좌석마킹AdjustFlightInventoryRQ.xsd / AdjustFlightInventoryRS.xsd
예약CreateBookingRQ.xsd / CreateBookingRS.xsd
운임변경ModifyBookingRQ.xsd / ModifyBookingRS.xsd
발권/취소/변경SaveModifyBookingRQ.xsd / SaveModifyBookingRS.xsd
조회RetrieveBookingRQ.xsd / RetrieveBookingRS.xsd
취소정보CancelBookingRQ.xsd / CancelBookingRS.xsd
PNR 분리SplitPnrRQ.xsd / SplitPnrRS.xsd
수하물ListBaggageServicesRQ.xsd / ListBaggageServicesRS.xsd
좌석맵ShowSeatMapRQ.xsd / ShowSeatMapRS.xsd
대리점크레딧RetrieveAgencyCreditRQ.xsd / RetrieveAgencyCreditRS.xsd
<!-- AirAvailabilityRQ.xsd 발췌 — 코드의 고정값과 정확히 대응 -->
<xs:schema targetNamespace="http://www.ibsplc.com/iRes/simpleTypes/"
           xmlns:urn="http://www.ibsplc.com/iRes/simpleTypes/" ...>
   <xs:include schemaLocation="SimpleTypes.xsd"/>
   <xs:include schemaLocation="CommonTypes.xsd"/>
   <xs:element name="AirAvailabilityRQ">
      <xs:complexType><xs:sequence>
         <xs:element name="AirlineCode" type="xs:string" nillable="false"/>  <!-- Max Length: 6 -->
         <xs:element name="AvailabilitySearches" type="urn:NearBySearchType" maxOccurs="unbounded"/>
         <xs:element name="PaxCountDetails"     type="urn:PaxCountType"     maxOccurs="unbounded"/>
         <xs:element name="TripType"            type="urn:TripType"/>
         ...

XSD는 매핑 검증의 1차 사료

코드 필드명이 의심스러우면 동일 이름의 *RQ.xsd/*RS.xsd에서 엘리먼트명·maxOccurs(배열 여부)·타입을 확인하면 된다. CreateBookingMigRQ.xsd·RetrieveBookingArcRQ.xsd처럼 어댑터가 안 쓰는 변형 스키마도 함께 들어있으니 어댑터 DTO가 실제 쓰는 오퍼레이션(5절 표)만 신뢰하라.


7. 실제 전문 예시

mockData 없음

src/test/resources/mockData/jinair/ 디렉터리 및 진에어용 샘플 XML/JSON 전문은 저장소에 존재하지 않는다. 따라서 실 응답 캡처 대신, 위 DTO 어노테이션으로부터 재구성한 형태로 예시를 제시한다(필드명·고정값은 코드 검증 기반).

요청(개념도, search 기준):

{
  "officeId": "<AgencyCode>",
  "service": "getAirAvailability",
  "bodyXml": "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><sim:AirAvailabilityRQ xmlns:sim=\"http://www.ibsplc.com/iRes/simpleTypes/\"><AirlineCode>LJ</AirlineCode><AvailabilitySearches><Origin>ICN</Origin><Destination>NRT</Destination><TravelDate>2026-06-10</TravelDate></AvailabilitySearches><PaxCountDetails><PaxType>ADT</PaxType><PaxCount>1</PaxCount></PaxCountDetails><TripType>OW</TripType><FareLevels>IS</FareLevels><PointOfPurchase>KR</PointOfPurchase><BookingChannel><ChannelType>API</ChannelType><Channel>OII</Channel><Locale>ko_KR</Locale></BookingChannel></sim:AirAvailabilityRQ></SOAP-ENV:Body></SOAP-ENV:Envelope>"
}

응답(개념도):

{
  "errorMessage": null,
  "errorType": null,
  "service": "getAirAvailability",
  "CLIENT_SESSION_ID": "....",
  "bodyXml": "<SOAP-ENV:Envelope ...><SOAP-ENV:Body><AirAvailabilityRS>...<OriginDestinationInfo Origin=\"ICN\" Destination=\"NRT\" PricingUnitID=\"1\">...</OriginDestinationInfo></AirAvailabilityRS></SOAP-ENV:Body></SOAP-ENV:Envelope>"
}

FareRule(순수 JSON 봉투):

{ "officeId": "<AgencyCode>", "service": "fareRegulationData",
  "bodyXml": [ { "fareTypeCdList": ["..."], "itnTypCd": "OW", "rbd": "Y",
                "departureAirportCd": "ICN", "arrivalAirportCd": "NRT",
                "departureDt": "20260610", "currencyCd": "KRW", "langCd": "KOR" } ] }

8. 타임아웃·예외 전파(전송 계층 관점)

  • 타임아웃: JinairClientClientSupport(searchTimeout = 15000, defaultTimeout = 40000). 검색만 별도 searchClient(JinairClient.kt:97).
  • 발권/재발권 타임아웃 복구: 응답이 isTimeoutError면 즉시 실패 처리하지 않고 retrieve(pnr)로 PNR을 재조회해 실제 발권 여부를 확인한다(JinairClient.kt:478-480, 1010-1012). 전송 실패가 곧 비즈니스 실패가 아니라는 LCC 결제 특유의 멱등성 처리.
  • 취소 계열은 @Retryable(maxAttempts=3/2, Backoff 5s, exceptionExpression="@jinairClient.shouldCancelRetryable(#root)")ApiException.retryable가 true일 때만 재시도(BKG_CONCURRECY_01 등 잠금 충돌). 메시지큐가 아닌 예외 전파 + Spring Retry가 상태 회복 메커니즘이다. → resilience-and-events

9. 다른 공급사와의 비교 (전송 관점)

구분Jin AirT’way(tway-protocol)순수 GDS(Amadeus/Sabre/Galileo)
분류LCC/RESTLCC/RESTGDS/SOAP
실제 전송SOAP-in-JSON(bodyXml) + 일부 순수 JSONREST + SEED 암호화 인증직접 SOAP Envelope
세션없음(stateless)없음stateful 세션/토큰
인증x-api-key + AgencyCodeTwaySEED.jar SEEDEPR/PCC/WS-Security
카드 암호화RSA(nonce)+AES(SHA-256 파생키)SEED(해당 없음)
백엔드 플랫폼IBS Software iRes자체GDS

더 보기