Singapore Airlines 검색 API 심층 분석

1. API 엔드포인트 개요

1.1 검색 API

  • 엔드포인트: POST /internals/SINGAPOREAIR/search
  • API 타입: SOAP API (AirShoppingRQ v18.2, IATA NDC)
  • Controller: SingaporeairSearchController.kt
  • Service: SingaporeairFlightSearchService.kt:34-93
  • Client: SingaporeairClient.kt:65-123

1.2 상세 조회 API

  • 엔드포인트: GET /internals/SINGAPOREAIR/search
  • Service: SingaporeairFlightSearchService.kt:95-99

2. 핵심 비즈니스 로직 플로우

2.1 전체 처리 흐름도

flowchart TD
    A[요청 수신] --> B[캐시 키 생성]
    B --> C{Redis 캐시<br/>확인}
    C -->|캐시 히트| D[캐시된 결과 반환]
    C -->|캐시 미스| E[공항 필터링<br/>makeOriginDestinationsFilterByRoutes]
    E --> F{SQ 지원<br/>노선 존재?}
    F -->|없음| G[빈 리스트 반환]
    F -->|있음| H[Cartesian Product<br/>모든 조합 생성]
    H --> I[병렬 검색 실행<br/>pmap]
    I --> J[SOAP 요청<br/>AirShoppingRQ]
    J --> K{에러 체크}
    K -->|710/367 에러| L[빈 리스트 반환]
    K -->|기타 에러| M[예외 발생]
    K -->|정상| N[결과 파싱<br/>toFareItineraries]
    N --> O[필터링 1:<br/>NonAir 제거]
    O --> P[필터링 2:<br/>Non-SQ 항공사 제거]
    P --> Q[중복 제거<br/>distinctBy id]
    Q --> R{캐싱 조건<br/>확인}
    R -->|useCache=true &<br/>결과 존재| S[Redis 저장]
    R -->|조건 미충족| T[응답 반환]
    S --> T[응답 반환]

    style C fill:#A8D5BA,stroke:#333,stroke-width:2px,color:#000
    style D fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000
    style E fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style I fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000
    style J fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000
    style K fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000
    style L fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000

2.2 단계별 상세 분석

Step 1: 캐시 키 생성

위치: Controller 레벨

val requestKey = CacheKeyGenerator.generateSearchRequestKey(
    supplier = Supplier.SINGAPOREAIR,
    request = request
)

캐시 키 구조:

SINGAPOREAIR_SEARCH_{originDestinations}_{cabins}_{passenger}_{timestamp}

Step 2: Redis 캐시 확인

위치: SingaporeairFlightSearchService.kt:46-48

flightSearchKeyRepository.findKey(requestKey = requestKey)?.let { key ->
    fareItineraryRepository.getFareItineraries(key = key)
}

캐시 히트 시: 즉시 결과 반환 (성능 최적화) 캐시 미스 시: 새로운 검색 수행

Step 3: 공항 필터링 (SQ 지원 노선만)

위치: SingaporeairFlightSearchService.kt:51-55

val filteredOriginDestinations = makeOriginDestinationsFilterByRoutes(originDestinationInfos)
 
if (filteredOriginDestinations.isEmpty()) {
    return emptyList()
}

필터링 로직: AirportUtils.kt

fun makeOriginDestinationsFilterByRoutes(
    originDestinationLocationInfos: List<OriginDestinationLocationInfo>
): List<List<OriginDestination>> {
    return originDestinationLocationInfos.map { originDestinationLocationInfo ->
        // SQ가 지원하는 노선만 필터링
        val originAirports = originDestinationLocationInfo.origin.airports.filter { origin ->
            originDestinationLocationInfo.destination.airports.any { destination ->
                isValidRoute(origin, destination)  // SQ 노선 확인
            }
        }
 
        // 지원되지 않는 노선이면 빈 리스트 반환
        if (originAirports.isEmpty()) return@map emptyList()
 
        // 지원되는 공항 조합 생성
        originAirports.flatMap { origin ->
            originDestinationLocationInfo.destination.airports.map { destination ->
                OriginDestination(
                    originType = LocationType.AIRPORT,
                    origin = origin,
                    destinationType = LocationType.AIRPORT,
                    destination = destination,
                    departureDate = originDestinationLocationInfo.departureDate
                )
            }
        }
    }
}

특징:

  • Singapore Airlines가 지원하는 노선만 필터링
  • 지원하지 않는 노선은 즉시 빈 리스트 반환 (불필요한 API 호출 방지)
  • Amadeus/Sabre/Galileo와의 차이: 직연동이므로 사전 필터링 필수

Step 4: Cartesian Product (공항 조합 생성)

위치: SingaporeairFlightSearchService.kt:57-60

withBlocking(Dispatchers.IO) {
    filteredOriginDestinations
        .cartesianProduct()  // 모든 조합 생성
        .pmap { originDestinations ->
            ...
        }
}

예시:

입력: SEL (서울) → SIN (싱가포르)
↓
확장: [ICN] → [SIN]  // SQ가 지원하는 공항만
↓
조합:
  - ICN → SIN

GDS와의 차이:

  • GDS (Amadeus/Sabre/Galileo): 모든 공항 조합 시도
  • Singapore Air: SQ가 지원하는 노선만 조합

Step 5: 병렬 검색 실행

위치: SingaporeairFlightSearchService.kt:60-76

.pmap { originDestinations ->  // 병렬 매핑 (Coroutines)
    singaporeairClient.search(
        requestKey = requestKey,
        key = key,
        preference = SearchPreferenceInfo.of(preferences.first()),  // multiFare 옵션 없음
        originDestRequests = originDestinations.map { OriginDestRequest.of(it) },
        cabins = cabins,
        onlyDirect = onlyDirect,
        onlyFreeBaggageInclude = onlyFreeBaggageInclude,
        logging = logging
    )
}.onFailure { exceptions, successes ->
    // 모든 요청이 실패한 경우에만 예외 발생
    if (successes.isEmpty()) {
        throw exceptions.first()
    }
}.getOrEmpty()

병렬 처리 특징

  • 도구: Kotlin Coroutines (Dispatchers.IO)
  • 패턴: pmap (parallel map)
  • 실패 전략: 일부 성공 시 계속 진행
  • 성능: I/O 대기 시간 최소화

multiFare 옵션 없음:

preference = SearchPreferenceInfo.of(preferences.first())  // 첫 번째만 사용
  • GDS와 달리 NDC는 multiFare 옵션을 지원하지 않음
  • 첫 번째 preference만 사용

Step 6: SOAP 요청 처리

위치: SingaporeairClient.kt:65-123

fun search(
    requestKey: String,
    key: String,
    preference: SearchPreferenceInfo,
    originDestRequests: List<OriginDestRequest>,
    cabins: List<CabinType>,
    onlyDirect: Boolean,
    onlyFreeBaggageInclude: Boolean,
    logging: Boolean,
): List<FareItinerary> {
    val singaporeairApiProperties = singaporeairProperties.getApiProperties()
    val passengerInfo = preference.passenger
 
    // 1. AirShoppingRQ 생성
    val request = AirShoppingRQ.of(
        adult = passengerInfo.adult,
        child = passengerInfo.child,
        infant = passengerInfo.infant,
        cabins = cabins,
        originDestRequests = originDestRequests,
        iataNumber = singaporeairApiProperties.iataCode,
        agencyName = singaporeairApiProperties.agencyName
    )
 
    // 2. SOAP 요청
    return singaporeairApiProperties.endpoint
        .post(request)
        .client(searchClient)  // 15초 타임아웃
        .header(getHeaderMap(request))
        .requestBodyConvert(soapRequestBodyConverter(singaporeairApiProperties))
        .log(enableSearchLog.or(logging))
        .execute<AirShoppingRS>(soapBodyDeserializerOf(logger, objectMapper))
        .fold(
            success = { airShoppingRS ->
                airShoppingRS.checkError { code, message ->
                    // 710: NO FARE FOUND FOR REQUESTED ITINERARY (정상)
                    // 367: NO ACTIVE ITINERARY IN THE AIRLINE PROFILE (정상)
                    when (code) {
                        "710", "367" -> {}  // 빈 리스트 반환
                        else -> throw InternationalAdapterException(
                            ErrorMessage.SEARCH_FAILED,
                            code,
                            message,
                            originDestRequests.joinToString { "${it.originDepRequest.iataLocationCode} ${it.destArrivalRequest.iataLocationCode} ${it.originDepRequest.date}" }
                        ).capture()
                    }
                }
                airShoppingRS.response?.toFareItineraries(
                    requestKey = requestKey,
                    key = key,
                    onlyDirect = onlyDirect,
                    onlyFreeBaggageInclude = onlyFreeBaggageInclude,
                    cabins = cabins,
                )?.filterNot {
                    it.isNonAir() || it.isNonSingaporeAirlineSchedule()
                } ?: emptyList()
            },
            failure = { failure ->
                throw failure.handleSoapFaultException(ErrorMessage.SEARCH_FAILED)
            }
        )
}

SOAP 요청 구조:

<soapenv:Envelope>
  <soapenv:Header>
    <AMA_SecurityHostedUser>
      <UserID POS_Type="1" RequestorType="U" PseudoCityCode="{pcc}" AgentDutyCode="SU">
        <RequestorID>
          <CompanyName>TRIPLE</CompanyName>
        </RequestorID>
      </UserID>
    </AMA_SecurityHostedUser>
    <Security>
      <UsernameToken>
        <Username>{username}</Username>
        <Password Type="PasswordDigest">{digest}</Password>
        <Nonce>{nonce}</Nonce>
        <Created>{timestamp}</Created>
      </UsernameToken>
    </Security>
    <Action>AirShopping</Action>
    <MessageID>urn:uuid:{uuid}</MessageID>
    <To>{endpoint}</To>
  </soapenv:Header>
  <soapenv:Body>
    <AirShoppingRQ Version="18.2">
      <PointOfSale>
        <Country>{countryCode}</Country>
        <AgencyID>{iataCode}</AgencyID>
        <AgencyName>{agencyName}</AgencyName>
      </PointOfSale>
      <Traveler>
        <AnonymousTraveler>
          <PTC Quantity="{adult}">ADT</PTC>
          <PTC Quantity="{child}">CHD</PTC>
          <PTC Quantity="{infant}">INF</PTC>
        </AnonymousTraveler>
      </Traveler>
      <CoreQuery>
        <OriginDestinations>
          <OriginDestination>
            <Departure>
              <AirportCode>{origin}</AirportCode>
              <Date>{departureDate}</Date>
            </Departure>
            <Arrival>
              <AirportCode>{destination}</AirportCode>
            </Arrival>
            <MarketingCarrierAirlineID>SQ</MarketingCarrierAirlineID>
          </OriginDestination>
        </OriginDestinations>
      </CoreQuery>
      <Preference>
        <CabinPreferences>
          <CabinType>
            <Code>{cabinCode}</Code>
          </CabinType>
        </CabinPreferences>
      </Preference>
    </AirShoppingRQ>
  </soapenv:Body>
</soapenv:Envelope>

타임아웃 설정:

// ClientSupport.kt
searchTimeout = 15000  // 15초 (GDS는 30초)

GDS와의 차이:

  • GDS: 30초 타임아웃
  • Singapore Air: 15초 타임아웃 (직연동이므로 더 빠름)

Step 7: 에러 처리

위치: SingaporeairClient.kt:96-106

airShoppingRS.checkError { code, message ->
    when (code) {
        "710" -> {}  // NO FARE FOUND FOR REQUESTED ITINERARY (정상)
        "367" -> {}  // NO ACTIVE ITINERARY IN THE AIRLINE PROFILE (정상)
        else -> throw InternationalAdapterException(
            ErrorMessage.SEARCH_FAILED,
            code,
            message,
            originDestRequests.joinToString { ... }
        ).capture()
    }
}

에러 코드별 처리:

코드메시지처리
710NO FARE FOUND FOR REQUESTED ITINERARY정상 (빈 리스트 반환)
367NO ACTIVE ITINERARY IN THE AIRLINE PROFILE정상 (빈 리스트 반환)
기타예외 발생 (SEARCH_FAILED)

SoapFault 처리:

failure = { failure ->
    throw failure.handleSoapFaultException(ErrorMessage.SEARCH_FAILED)
}

Step 8: 결과 필터링

8-1. NonAir 제거 위치: SingaporeairClient.kt:125-131

private fun FareItinerary.isNonAir(): Boolean {
    return this.schedules.any { schedule ->
        schedule.segments.any {
            it.equipmentType != null && NonAirEquipment.existValueOf(it.equipmentType)
        }
    }
}

NonAir Equipment:

enum class NonAirEquipment {
    BUS,   // 버스
    TRAIN, // 기차
    SHIP,  // 선박
    ;
}

8-2. Non-SQ 항공사 제거 위치: SingaporeairClient.kt:133-137

private fun FareItinerary.isNonSingaporeAirlineSchedule(): Boolean {
    return this.schedules.any { schedule ->
        schedule.segments.any { it.marketingCarrier != "SQ" }
    }
}

특징:

  • Singapore Airlines (SQ) 마케팅 편만 허용
  • 타 항공사 공동 운항편 제외
  • GDS와의 차이: GDS는 다수 항공사 지원, SQ는 직연동으로 SQ만 지원

8-3. 중복 제거 위치: SingaporeairFlightSearchService.kt:77

.distinctBy { it.id }

Step 9: Redis 캐싱

위치: SingaporeairFlightSearchService.kt:79-85

.also {
    if (useCache && it.isNotEmpty()) {
        flightSearchKeyRepository.addKey(requestKey = requestKey, key = key)
        fareItineraryRepository.saveFareItineraries(
            key = key,
            values = it.associateBy { fareItinerary -> fareItinerary.itemKey }
        )
    }
}

캐싱 조건:

  • useCache = true
  • 검색 결과가 비어있지 않음

캐시 구조:

  • Key: SINGAPOREAIR_{key}_{timestamp}
  • Value: Map<String, FareItinerary> (itemKey → FareItinerary)
  • Serializer: Jackson (기본 JSON 직렬화)

GDS와의 차이:

공급사압축 방식
Singapore Air없음 (기본 JSON)
GalileoGzip
AmadeusSnappy
Sabre없음

Step 10: 항공사 필터링 및 결과 수 제한

위치: SingaporeairFlightSearchService.kt:86-92

.filterIncludedAirline(airlines = airlines)
.let {
    if (advancedOption?.ratio?.total != null) {
        it.take(advancedOption.ratio.total)
    } else {
        it
    }
}
 
private fun List<FareItinerary>.filterIncludedAirline(airlines: List<String>?): List<FareItinerary> {
    return if (airlines == null) {
        this
    } else {
        this.filter { airlines.contains(it.validatingCarrier) }
    }
}

필터링 로직:

  • airlines 파라미터가 있으면 해당 항공사만 필터링
  • advancedOption.ratio.total이 있으면 결과 수 제한

3. AirShoppingRQ/RS 상세 분석

3.1 AirShoppingRQ 구조

위치: request/AirShoppingRQ.kt

주요 필드:

data class AirShoppingRQ(
    val pointOfSale: PointOfSale,
    val travelers: Travelers,
    val coreQuery: CoreQuery,
    val preference: Preference?,
) : SingaporeairRequest {
    override val action = "http://www.iata.org/IATA/2015/00/2018.2/AirShoppingRQ"
 
    companion object {
        fun of(
            adult: Int,
            child: Int,
            infant: Int,
            cabins: List<CabinType>,
            originDestRequests: List<OriginDestRequest>,
            iataNumber: String,
            agencyName: String,
        ): AirShoppingRQ {
            return AirShoppingRQ(
                pointOfSale = PointOfSale(
                    country = Country(countryCode = "KR"),
                    agencyID = AgencyID(value = iataNumber),
                    agencyName = AgencyName(value = agencyName),
                ),
                travelers = Travelers(
                    anonymousTraveler = AnonymousTraveler(
                        ptc = listOf(
                            if (adult > 0) PTC(quantity = adult, value = "ADT") else null,
                            if (child > 0) PTC(quantity = child, value = "CHD") else null,
                            if (infant > 0) PTC(quantity = infant, value = "INF") else null,
                        ).filterNotNull()
                    )
                ),
                coreQuery = CoreQuery(
                    originDestinations = OriginDestinations(
                        originDestination = originDestRequests.map { OriginDestination.of(it) }
                    )
                ),
                preference = Preference(
                    cabinPreferences = CabinPreferences(
                        cabinType = cabins.map { CabinTypeCode(code = it.singaporeairCabinCode) }
                    )
                )
            )
        }
    }
}

PointOfSale (판매 지점):

data class PointOfSale(
    val country: Country,           // 판매 국가 (KR)
    val agencyID: AgencyID,         // IATA 번호
    val agencyName: AgencyName,     // 여행사명
)

Travelers (탑승객 정보):

data class Travelers(
    val anonymousTraveler: AnonymousTraveler,  // 익명 탑승객
)
 
data class AnonymousTraveler(
    val ptc: List<PTC>,  // Passenger Type Code
)
 
data class PTC(
    @JacksonXmlProperty(isAttribute = true)
    val quantity: Int,      // 인원 수
    @JacksonXmlText
    val value: String,      // ADT, CHD, INF
)

CoreQuery (검색 조건):

data class CoreQuery(
    val originDestinations: OriginDestinations,
)
 
data class OriginDestinations(
    val originDestination: List<OriginDestination>,
)
 
data class OriginDestination(
    val departure: Departure,
    val arrival: Arrival,
    val marketingCarrierAirlineID: MarketingCarrierAirlineID?,  // SQ 고정
) {
    companion object {
        fun of(originDestRequest: OriginDestRequest): OriginDestination {
            return OriginDestination(
                departure = Departure(
                    airportCode = AirportCode(value = originDestRequest.originDepRequest.iataLocationCode),
                    date = Date(value = originDestRequest.originDepRequest.date),
                ),
                arrival = Arrival(
                    airportCode = AirportCode(value = originDestRequest.destArrivalRequest.iataLocationCode),
                ),
                marketingCarrierAirlineID = MarketingCarrierAirlineID(value = "SQ"),  // SQ 고정
            )
        }
    }
}

Preference (선호 조건):

data class Preference(
    val cabinPreferences: CabinPreferences,
)
 
data class CabinPreferences(
    val cabinType: List<CabinTypeCode>,
)
 
data class CabinTypeCode(
    val code: Code,
)
 
data class Code(
    @JacksonXmlText
    val value: String,  // M (이코노미), C (비즈니스), F (퍼스트)
)

Cabin Code 매핑:

val CabinType.singaporeairCabinCode: String
    get() = when (this) {
        CabinType.ECONOMY, CabinType.PREMIUM_ECONOMY -> "M"
        CabinType.BUSINESS -> "C"
        CabinType.FIRST -> "F"
        else -> "M"
    }

3.2 AirShoppingRS 구조

위치: response/AirShoppingRS.kt

주요 필드:

data class AirShoppingRS(
    val response: Response?,
    val errors: List<Error>?,
) {
    data class Response(
        val offersGroup: OffersGroup?,
        val dataList: DataList,
        val warnings: List<Warning>?,
    )
}

OffersGroup (제안 그룹):

data class OffersGroup(
    val carrierOffers: CarrierOffers,
)
 
data class CarrierOffers(
    val offer: List<Offer>,
)
 
data class Offer(
    val offerId: String,
    val offerItems: List<OfferItem>,
    val totalPrice: TotalPrice,
    val validatingCarrier: ValidatingCarrier,
)
 
data class OfferItem(
    val offerItemId: String,
    val price: Price,
    val services: List<Service>,
    val fareDetails: List<FareDetail>?,
)

DataList (참조 데이터):

data class DataList(
    val paxSegments: List<PaxSegment>,      // 여정 세그먼트
    val paxJourneys: List<PaxJourney>,      // 여정
    val priceClasses: List<PriceClass>,     // 운임 클래스 (MiniRule 포함)
    val baggageAllowances: List<BaggageAllowance>?,  // 수하물
)

toFareItineraries 변환:

fun Response.toFareItineraries(
    requestKey: String,
    key: String,
    onlyDirect: Boolean,
    onlyFreeBaggageInclude: Boolean,
    cabins: List<CabinType>,
): List<FareItinerary> {
    return this.offersGroup?.carrierOffers?.offer?.flatMap { offer ->
        offer.offerItems.map { offerItem ->
            FareItinerary(
                requestKey = requestKey,
                key = key,
                id = "${offer.offerId}_${offerItem.offerItemId}",
                itemKey = offerItem.offerItemId,
                validatingCarrier = offer.validatingCarrier.code,
                schedules = offerItem.services.map { service ->
                    service.toSchedule(dataList.paxSegments, dataList.paxJourneys)
                },
                passengerFares = offerItem.toPassengerFares(cabins),
                ...
            )
        }
    }?.filter { fareItinerary ->
        // Direct 필터링
        if (onlyDirect) {
            fareItinerary.schedules.all { it.segments.size == 1 }
        } else {
            true
        }
    }?.filter { fareItinerary ->
        // 무료 수하물 필터링
        if (onlyFreeBaggageInclude) {
            fareItinerary.passengerFares.all { it.freeBaggage != null }
        } else {
            true
        }
    } ?: emptyList()
}

4. 성능 최적화

4.1 병렬 처리 (pmap)

// SingaporeairFlightSearchService.kt:60
.pmap { originDestinations ->
    singaporeairClient.search(...)
}

성능 향상:

  • I/O 대기 시간 최소화
  • 다수 노선 검색 시 효과적

예시:

순차 처리: ICN-SIN (2초) + GMP-SIN (2초) = 4초
병렬 처리: max(ICN-SIN (2초), GMP-SIN (2초)) = 2초

4.2 Redis 캐싱

// SingaporeairFlightSearchService.kt:46-48
flightSearchKeyRepository.findKey(requestKey = requestKey)?.let { key ->
    fareItineraryRepository.getFareItineraries(key = key)
}

캐시 히트 비율 향상:

  • 동일 조건 재검색 시 즉시 반환
  • API 호출 횟수 감소

4.3 사전 필터링 (SQ 지원 노선)

// SingaporeairFlightSearchService.kt:51-55
val filteredOriginDestinations = makeOriginDestinationsFilterByRoutes(originDestinationInfos)
 
if (filteredOriginDestinations.isEmpty()) {
    return emptyList()
}

성능 향상:

  • 지원하지 않는 노선은 API 호출 전에 필터링
  • 불필요한 API 호출 방지

4.4 타임아웃 최적화

// ClientSupport.kt
searchTimeout = 15000  // 15초

GDS 대비 50% 단축:

  • GDS: 30초
  • Singapore Air: 15초
  • 직연동으로 인한 빠른 응답 시간 활용

5. 에러 처리 및 재시도

5.1 에러 코드별 처리

// SingaporeairClient.kt:96-106
airShoppingRS.checkError { code, message ->
    when (code) {
        "710", "367" -> {}  // 정상 (빈 리스트 반환)
        else -> throw InternationalAdapterException(...)
    }
}

5.2 SoapFault 처리

// ClientSupport.kt
fun Failure.handleSoapFaultException(errorMessage: ErrorMessage, vararg args: Any?): InternationalAdapterException {
    return if (this.exception is SoapFaultException) {
        InternationalAdapterException(
            errorMessage,
            *args,
            this.exception.faultCode,
            this.exception.faultString
        )
    } else {
        InternationalAdapterException(errorMessage, *args, this.exception)
    }
}

5.3 부분 실패 허용

// SingaporeairFlightSearchService.kt:71-75
.onFailure { exceptions, successes ->
    if (successes.isEmpty()) {
        throw exceptions.first()
    }
}.getOrEmpty()

실패 전략:

  • 모든 요청 실패: 첫 번째 예외 발생
  • 일부 성공: 성공한 결과만 반환

6. GDS와의 차이점 비교

항목Singapore Air (NDC)AmadeusSabreGalileo
API 타입SOAP (NDC)SOAPRESTREST
인증WS-Security Password DigestSession TokenOAuth 2.0OAuth 2.0
타임아웃15초30초30초30초
공항 필터링사전 필터링 (SQ 노선)사후 필터링사후 필터링사후 필터링
항공사SQ only다수 항공사다수 항공사다수 항공사
NonAir 필터링있음 (BUS/TRAIN)있음있음있음
Redis 압축없음 (기본 JSON)Snappy없음Gzip
병렬 처리pmap (Cartesian Product)pmap (Cartesian Product)pmappmap
에러 코드710, 367 (정상)특정 에러 코드특정 에러 코드특정 에러 코드
multiFare미지원지원지원미지원

7. 트러블슈팅

7.1 일반적인 문제

문제원인해결 방법
710 에러NO FARE FOUND정상 (빈 리스트 반환)
367 에러NO ACTIVE ITINERARY정상 (빈 리스트 반환)
NonAir 항목 포함BUS/TRAIN 구간자동 필터링 (isNonAir)
Non-SQ 항공사 포함타 항공사 마케팅 편자동 필터링 (isNonSingaporeAirlineSchedule)
캐시 미스새로운 검색 조건정상 (새로운 검색 수행)
타임아웃 (15초)네트워크/시스템 지연재시도 또는 조건 변경

7.2 디버깅 팁

  • SOAP 요청/응답 로깅 활성화 (logging = true)
  • Redis 캐시 확인 (검색 키, FareItinerary)
  • SQ 지원 노선 확인 (사전 필터링)
  • NonAir/Non-SQ 필터링 확인

7.3 성능 이슈

  • 캐시 히트 비율 확인
  • 병렬 처리 (pmap) 활용 확인
  • 사전 필터링 (SQ 노선) 확인

8. 참고 자료

8.1 주요 클래스

  • SingaporeairClient.kt: SOAP 통신 클라이언트
  • SingaporeairFlightSearchService.kt: 검색 서비스
  • AirShoppingRQ.kt: 검색 요청 DTO
  • AirShoppingRS.kt: 검색 응답 DTO
  • AirportUtils.kt: 공항 필터링 유틸

8.2 관련 문서

  • IATA NDC 18.2 Documentation
  • Singapore Airlines NDC Implementation Guide
  • WS-Security Username Token Profile

이 문서는 Triple Air International Adapter 프로젝트의 Singapore Airlines 검색 API 심층 분석 문서입니다.