Phase 7: Galileo 큐 관리 API 심층 분석
1. API 엔드포인트 개요
1.1 큐 관련 엔드포인트
| 엔드포인트 | 메서드 | API 타입 | 기능 | 위치 |
|---|---|---|---|---|
/internals/GALILEO/queue | GET | SOAP | 큐 PNR 목록 조회 | GalileoQueueController.kt:15-20 |
/internals/GALILEO/queue | DELETE | SOAP | 큐 PNR 제거 | GalileoQueueController.kt:22-31 |
2. 큐 시스템 개요
2.1 큐(Queue)란?
GDS 큐 시스템:
- GDS에서 PNR을 관리하는 대기열
- 발권 대기, 스케줄 변경 등 이벤트 발생 시 자동 등록
- 운영팀이 수동으로 처리해야 하는 PNR 모음
2.2 Target Queue Numbers
위치: GalileoQueueService.kt:27-29
companion object {
val TARGET_QUEUE_NUMBERS = listOf("21", "22", "23")
}큐 번호 의미:
- 21: 발권 대기 (Ticketing Queue)
- 22: 스케줄 변경 (Schedule Change Queue)
- 23: 기타 알림 (Notification Queue)
3. 큐 PNR 조회 (getQueuePnrs)
3.1 전체 조회 플로우
flowchart TD A[큐 조회 요청] --> B[PCC 속성 맵 생성<br/>getQueuePropertyMap] B --> C[병렬 큐 카운트 조회<br/>getCountInQueue] C --> D[TARGET_QUEUE_NUMBERS<br/>필터링] D --> E{큐 카운트<br/>존재?} E -->|없음| F[빈 리스트 반환] E -->|있음| G[PCC별 병렬 처리<br/>pmap] G --> H[큐 번호별<br/>PNR 조회] H --> I[getPnrsInQueue<br/>API 호출] I --> J[QueuePnrInfo<br/>생성] J --> K[평탄화<br/>flatten] K --> L[큐 PNR 목록 반환] %% 에러 플로우 C --> ERR{예외 발생} I --> ERR ERR --> M[Slack 알림<br/>sendQueueFail] M --> N[빈 리스트 반환] style E fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style G fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style L fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000 style ERR fill:#E8B4B8,stroke:#333,stroke-width:2px,color:#000 style M fill:#FFE5B4,stroke:#333,stroke-width:2px,color:#000
3.2 단계별 상세 분석
Step 1: PCC 속성 맵 생성
위치: GalileoQueueService.kt:95-123
private fun getQueuePropertyMap(): Map<String, QueueRequestProperty> {
return buildMap {
val galileoApiProperties =
galileoProperties.channels.flatMap { channel -> channel.funnels }.distinctBy { it.pcc }
putAll(
galileoApiProperties.map {
it.pcc to QueueRequestProperty(
pcc = it.pcc,
endpoint = it.soap.endpoint,
userName = it.soap.userName,
password = it.soap.password,
branchCode = it.soap.branchCode,
)
}
)
// offline pcc
galileoApiProperties.first().soap.let {
set(
it.offline.pcc, QueueRequestProperty(
pcc = it.offline.pcc,
endpoint = it.endpoint,
userName = it.offline.userName,
password = it.offline.password,
branchCode = it.offline.branchCode,
)
)
}
}
}PCC (Pseudo City Code):
- GDS 계정 식별자
- 채널/펀넬별 독립 PCC
- Offline PCC 추가 (오프라인 예약용)
QueueRequestProperty:
data class QueueRequestProperty(
val pcc: String,
val endpoint: String,
val userName: String,
val password: String,
val branchCode: String,
)Step 2: 큐 카운트 병렬 조회
위치: GalileoQueueService.kt:34-40
withBlocking(Dispatchers.IO) {
val queueCountsByPcc = queuePropertyMap.values
.pmap { property ->
val queueCounts = galileoClient.getCountInQueue(property)
.filter { it.number in TARGET_QUEUE_NUMBERS }
property.pcc to queueCounts
}.getOrEmpty().filter { (_, queueCounts) -> queueCounts.isNotEmpty() }
// ...
}병렬 처리:
- PCC별 병렬 큐 카운트 조회
pmap: 모든 PCC 동시 조회getOrEmpty: 실패 무시 (부분 성공 허용)
SOAP API: GalileoClient.getCountInQueue
- 엔드포인트:
/QueueService(GdsQueueCountRQ) - 반환:
List<QueueCount>
QueueCount:
data class QueueCount(
val number: String, // 큐 번호
val pnrCount: Int // PNR 개수
)Step 3: PNR 목록 병렬 조회
위치: GalileoQueueService.kt:42-51
queueCountsByPcc.pmap { (pcc, queueCounts) ->
queueCounts.flatMap { queueCount ->
val queueNumber = queueCount.number
logger.info("[${Supplier.GALILEO}] Get - Queue Number: $queueNumber, Counts: ${queueCount.pnrCount}, PCC: $pcc")
galileoClient.getPnrsInQueue(queuePropertyMap[pcc]!!, queueNumber = queueNumber)
.also { logger.info("[${Supplier.GALILEO}] GET - Queue Number: $queueNumber, PNRs: $it, PCC: $pcc") }
.map { pnr -> QueuePnrInfo(pnr = pnr, queueNumber = queueNumber, pccOid = pcc) }
}
}.getOrEmpty()SOAP API: GalileoClient.getPnrsInQueue
- 엔드포인트:
/QueueService(GdsQueueListRQ) - 파라미터:
queueNumber(큐 번호) - 반환:
List<String>(PNR 목록)
QueuePnrInfo:
data class QueuePnrInfo(
val pnr: String, // Provider PNR
val queueNumber: String, // 큐 번호
val pccOid: String // PCC
)Step 4: 에러 처리
위치: GalileoQueueService.kt:53-56
} catch (e: Exception) {
slackService.sendQueueFail(supplier = Supplier.GALILEO.name, reason = e.message)
emptyList()
}.flatten()에러 전략:
- Slack 알림
- 빈 리스트 반환 (예외 발생 안 함)
- 다음 스케줄에 재시도
4. 큐 PNR 제거 (remove)
4.1 전체 제거 플로우
flowchart TD A[큐 제거 요청] --> B[PCC 속성 맵 생성] B --> C[삭제 추적 Set 초기화<br/>totalDeletedQueues] C --> D[10개씩 Chunk<br/>chunked] D --> E[Chunk별 병렬 처리<br/>pmap] E --> F[removePnrsInQueue<br/>API 호출] F --> G{삭제 성공?} G -->|Yes| H[totalDeletedQueues<br/>추가] G -->|No| I[에러 저장<br/>error 변수] H --> J[모든 Chunk<br/>처리 완료?] I --> J J -->|Yes| K{남은 큐<br/>존재?} K -->|없음| L[true 반환<br/>성공] K -->|있음| M[Slack 알림<br/>sendQueueFail] M --> N{에러 존재?} N -->|Yes| O[예외 발생] N -->|No| P[QUEUE_REMOVE_FAILED] O --> Q[false 반환] P --> Q style D fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style E fill:#9FB4CE,stroke:#333,stroke-width:2px,color:#000 style K fill:#F4E4B1,stroke:#333,stroke-width:2px,color:#000 style L fill:#95D5A6,stroke:#333,stroke-width:2px,color:#000 style M fill:#FFE5B4,stroke:#333,stroke-width:2px,color:#000 style Q fill:#F39C9C,stroke:#333,stroke-width:2px,color:#000
4.2 단계별 상세 분석
Step 1: Chunk 처리
위치: GalileoQueueService.kt:64-72
return try {
withBlocking(Dispatchers.IO) {
val deletedQueues = queuePnrInfos.chunked(10).flatMap { chunkedTargetQueues ->
chunkedTargetQueues.pmap {
galileoClient.removePnrsInQueue(queuePropertyMap[it.pccOid]!!, queuePnrInfo = it)
}.onFailure { exceptions, _ ->
if (exceptions.isNotEmpty()) error = exceptions.first()
}.getOrEmpty()
}
if (deletedQueues.isNotEmpty()) totalDeletedQueues.addAll(deletedQueues)
}
// ...
}Chunk 크기: 10개
- 이유: API 부하 분산
- 병렬 처리: Chunk 내 병렬 제거
SOAP API: GalileoClient.removePnrsInQueue
- 엔드포인트:
/QueueService(GdsQueueRemoveRQ) - 파라미터:
queuePnrInfo(PNR + 큐번호 + PCC) - 반환:
QueuePnrInfo?(성공 시 반환)
Step 2: 삭제 검증
위치: GalileoQueueService.kt:77-86
val remainQueues = queuePnrInfos.toSet() - totalDeletedQueues
if (remainQueues.isNotEmpty()) {
logger.error("[${Supplier.GALILEO}] Remove Failed - Target Queues: $queuePnrInfos, Total Deleted Queues: $totalDeletedQueues, Remain Queues: $remainQueues")
if (error != null) {
throw error!!
} else {
throw InternationalAdapterException(ErrorMessage.QUEUE_REMOVE_FAILED, "queue remove failed")
}
}검증 로직:
- 요청 큐 - 삭제된 큐 = 남은 큐
- 남은 큐 존재 시 실패
- 에러 있으면 재발생, 없으면
QUEUE_REMOVE_FAILED
Step 3: 성공 로깅
위치: GalileoQueueService.kt:87-88
logger.info("[${Supplier.GALILEO}] Remove Success - Counts: ${queuePnrInfos.count()}, PNRs: ${queuePnrInfos.map { it.pnr }}")
trueStep 4: 에러 처리
위치: GalileoQueueService.kt:89-92
} catch (e: Exception) {
slackService.sendQueueFail(supplier = Supplier.GALILEO.name, reason = e.message)
false
}에러 전략:
- Slack 알림
false반환 (예외 발생 안 함)- 다음 스케줄에 재시도
5. SOAP API 상세
5.1 GdsQueueCountRQ (큐 카운트 조회)
fun getCountInQueue(property: QueueRequestProperty): List<QueueCount> {
val request = GdsQueueCountRQ.of(
targetBranch = property.branchCode,
pcc = property.pcc
)
return "${property.endpoint}/QueueService"
.post(request)
.authenticate(property.userName, property.password)
.execute<GalileoResponse<GdsQueueCountRS>>(...)
.fold(
success = { response ->
response.body!!.queueCounts
},
failure = { throw it.handleSoapFaultException(...) }
)
}5.2 GdsQueueListRQ (PNR 목록 조회)
fun getPnrsInQueue(property: QueueRequestProperty, queueNumber: String): List<String> {
val request = GdsQueueListRQ.of(
targetBranch = property.branchCode,
pcc = property.pcc,
queueNumber = queueNumber
)
return "${property.endpoint}/QueueService"
.post(request)
.authenticate(property.userName, property.password)
.execute<GalileoResponse<GdsQueueListRS>>(...)
.fold(
success = { response ->
response.body!!.pnrList
},
failure = { throw it.handleSoapFaultException(...) }
)
}5.3 GdsQueueRemoveRQ (PNR 제거)
fun removePnrsInQueue(property: QueueRequestProperty, queuePnrInfo: QueuePnrInfo): QueuePnrInfo? {
val request = GdsQueueRemoveRQ.of(
targetBranch = property.branchCode,
pcc = property.pcc,
queueNumber = queuePnrInfo.queueNumber,
pnr = queuePnrInfo.pnr
)
return "${property.endpoint}/QueueService"
.post(request)
.authenticate(property.userName, property.password)
.execute<GalileoResponse<GdsQueueRemoveRS>>(...)
.fold(
success = { response ->
if (response.body!!.success) queuePnrInfo else null
},
failure = { null }
)
}특징:
- 실패 시
null반환 (예외 발생 안 함) - 부분 성공 허용
6. 성능 최적화
6.1 병렬 처리 전략
flowchart LR A[PCC 1] --> B[큐 21] A --> C[큐 22] A --> D[큐 23] E[PCC 2] --> F[큐 21] E --> G[큐 22] H[PCC 3] --> I[큐 21] subgraph "병렬 처리 Level 1" A E H end subgraph "병렬 처리 Level 2" B C D F G I end
Level 1: PCC별 병렬 Level 2: 큐 번호별 병렬
효과:
순차 처리: 3 PCC * 3 큐 * 2초 = 18초
병렬 처리: max(2초) = 2초 (약 9배 향상)
6.2 Chunk 처리
위치: GalileoQueueService.kt:66
queuePnrInfos.chunked(10).flatMap { chunkedTargetQueues ->
chunkedTargetQueues.pmap { ... }
}Chunk 크기: 10개
- 목적: API 부하 분산
- 효과: 대량 제거 시 안정성 향상
예시:
100개 PNR 제거:
- Chunk 10개 (각 10개)
- 순차 처리 (Chunk 간)
- 병렬 처리 (Chunk 내)
총 시간: 10 * max(10개 병렬) = 10 * 1초 = 10초
6.3 부분 실패 허용
위치: GalileoQueueService.kt:69-71
.onFailure { exceptions, _ ->
if (exceptions.isNotEmpty()) error = exceptions.first()
}.getOrEmpty()전략:
- 일부 실패해도 성공한 것만 수집
- 최종 검증에서 남은 큐 체크
- 재시도 기회 보존
7. Amadeus/Sabre와의 비교
7.1 큐 API 비교
| 항목 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 큐 카운트 조회 | GdsQueueCountRQ | Queue_List | QueueAccessRQ (Count) |
| PNR 목록 조회 | GdsQueueListRQ | Queue_List | QueueAccessRQ (List) |
| PNR 제거 | GdsQueueRemoveRQ | Queue_RemoveItem | QueueAccessRQ (Remove) |
| Target Queue | 21, 22, 23 | 50 | 21, 22, 23 |
| 병렬 처리 | PCC + 큐 번호 | PCC만 | PCC + 큐 번호 |
| Chunk 크기 | 10개 | 없음 | 10개 |
| Offline PCC | 있음 (별도 추가) | 없음 | 없음 |
7.2 에러 처리 비교
| 에러 시나리오 | Galileo | Amadeus | Sabre |
|---|---|---|---|
| 조회 실패 | Slack + 빈 리스트 | 예외 발생 | Slack + 빈 리스트 |
| 제거 실패 | Slack + false | 예외 발생 | Slack + false |
| 부분 실패 | 성공한 것 반환 | 예외 발생 | 성공한 것 반환 |
| 재시도 | 다음 스케줄 | 즉시 실패 | 다음 스케줄 |
7.3 독특한 특징
Galileo 고유 특징
-
Offline PCC 지원:
- 오프라인 예약용 별도 PCC
- 자동 추가
-
2단계 병렬 처리:
- Level 1: PCC별
- Level 2: 큐 번호별
-
Chunk 처리:
- 10개씩 분할 제거
- API 부하 분산
-
부분 성공 허용:
- 일부 실패해도 계속 진행
- 최종 검증으로 재시도
-
Slack 통합:
- 실패 시 자동 알림
- 운영팀 즉시 대응
8. 주요 발견사항
8.1 Galileo 큐 시스템의 특징
- 2단계 병렬: PCC + 큐 번호
- Offline PCC: 오프라인 예약 지원
- Chunk 처리: 대량 제거 안정화
- 부분 성공: 실패 무시 후 재시도
- Slack 통합: 자동 알림
8.2 개선 가능 영역
1. Chunk 크기 설정화
현황: 하드코딩된 10개
queuePnrInfos.chunked(10)제안: 설정 파일로 관리
galileo:
queue:
chunk-size: 10
parallel-size: 52. 재시도 로직
현황: 재시도 없음
제안: 제거 실패 시 재시도
for (retry in 1..2) {
val result = galileoClient.removePnrsInQueue(...)
if (result != null) break
delay(1000)
}3. 큐 통계 로깅
현황: 최소 로깅
제안: 상세 통계
logger.info("[QUEUE_STATISTICS] " +
"Total: ${queueCountsByPcc.sumOf { (_, counts) -> counts.sumOf { it.pnrCount } }}, " +
"PCCs: ${queueCountsByPcc.size}, " +
"Queues: ${queueCountsByPcc.flatMap { (_, counts) -> counts }.size}")9. 참고 자료
9.1 주요 클래스
GalileoQueueService.kt: 큐 서비스 (19-124)GalileoClient.kt: SOAP 클라이언트 (큐 API)GdsQueueCountRQ.kt: 큐 카운트 요청GdsQueueListRQ.kt: PNR 목록 요청GdsQueueRemoveRQ.kt: PNR 제거 요청
9.2 주요 메소드
GalileoQueueService.getQueuePnrs(): 큐 PNR 조회 (31-57)GalileoQueueService.remove(): 큐 PNR 제거 (59-93)GalileoQueueService.getQueuePropertyMap(): PCC 속성 맵 (95-123)GalileoClient.getCountInQueue(): SOAP 큐 카운트GalileoClient.getPnrsInQueue(): SOAP PNR 목록GalileoClient.removePnrsInQueue(): SOAP PNR 제거
9.3 관련 API
- GdsQueueCountRQ: 큐 카운트 조회
- GdsQueueListRQ: PNR 목록 조회
- GdsQueueRemoveRQ: PNR 제거
이 문서는 Triple Air International Adapter 프로젝트의 Galileo 큐 관리 API 심층 분석 문서입니다.