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()
}
}에러 코드별 처리:
| 코드 | 메시지 | 처리 |
|---|---|---|
| 710 | NO FARE FOUND FOR REQUESTED ITINERARY | 정상 (빈 리스트 반환) |
| 367 | NO 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) |
| Galileo | Gzip |
| Amadeus | Snappy |
| 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) | Amadeus | Sabre | Galileo |
|---|---|---|---|---|
| API 타입 | SOAP (NDC) | SOAP | REST | REST |
| 인증 | WS-Security Password Digest | Session Token | OAuth 2.0 | OAuth 2.0 |
| 타임아웃 | 15초 | 30초 | 30초 | 30초 |
| 공항 필터링 | 사전 필터링 (SQ 노선) | 사후 필터링 | 사후 필터링 | 사후 필터링 |
| 항공사 | SQ only | 다수 항공사 | 다수 항공사 | 다수 항공사 |
| NonAir 필터링 | 있음 (BUS/TRAIN) | 있음 | 있음 | 있음 |
| Redis 압축 | 없음 (기본 JSON) | Snappy | 없음 | Gzip |
| 병렬 처리 | pmap (Cartesian Product) | pmap (Cartesian Product) | pmap | pmap |
| 에러 코드 | 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: 검색 요청 DTOAirShoppingRS.kt: 검색 응답 DTOAirportUtils.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 심층 분석 문서입니다.