Phase 7: Galileo 큐 관리 API 심층 분석

1. API 엔드포인트 개요

1.1 큐 관련 엔드포인트

엔드포인트메서드API 타입기능위치
/internals/GALILEO/queueGETSOAP큐 PNR 목록 조회GalileoQueueController.kt:15-20
/internals/GALILEO/queueDELETESOAP큐 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")
    }
}

검증 로직:

  1. 요청 큐 - 삭제된 큐 = 남은 큐
  2. 남은 큐 존재 시 실패
  3. 에러 있으면 재발생, 없으면 QUEUE_REMOVE_FAILED

Step 3: 성공 로깅

위치: GalileoQueueService.kt:87-88

logger.info("[${Supplier.GALILEO}] Remove Success - Counts: ${queuePnrInfos.count()}, PNRs: ${queuePnrInfos.map { it.pnr }}")
true

Step 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 비교

항목GalileoAmadeusSabre
큐 카운트 조회GdsQueueCountRQQueue_ListQueueAccessRQ (Count)
PNR 목록 조회GdsQueueListRQQueue_ListQueueAccessRQ (List)
PNR 제거GdsQueueRemoveRQQueue_RemoveItemQueueAccessRQ (Remove)
Target Queue21, 22, 235021, 22, 23
병렬 처리PCC + 큐 번호PCC만PCC + 큐 번호
Chunk 크기10개없음10개
Offline PCC있음 (별도 추가)없음없음

7.2 에러 처리 비교

에러 시나리오GalileoAmadeusSabre
조회 실패Slack + 빈 리스트예외 발생Slack + 빈 리스트
제거 실패Slack + false예외 발생Slack + false
부분 실패성공한 것 반환예외 발생성공한 것 반환
재시도다음 스케줄즉시 실패다음 스케줄

7.3 독특한 특징

Galileo 고유 특징

  1. Offline PCC 지원:

    • 오프라인 예약용 별도 PCC
    • 자동 추가
  2. 2단계 병렬 처리:

    • Level 1: PCC별
    • Level 2: 큐 번호별
  3. Chunk 처리:

    • 10개씩 분할 제거
    • API 부하 분산
  4. 부분 성공 허용:

    • 일부 실패해도 계속 진행
    • 최종 검증으로 재시도
  5. Slack 통합:

    • 실패 시 자동 알림
    • 운영팀 즉시 대응

8. 주요 발견사항

8.1 Galileo 큐 시스템의 특징

  1. 2단계 병렬: PCC + 큐 번호
  2. Offline PCC: 오프라인 예약 지원
  3. Chunk 처리: 대량 제거 안정화
  4. 부분 성공: 실패 무시 후 재시도
  5. Slack 통합: 자동 알림

8.2 개선 가능 영역

1. Chunk 크기 설정화

현황: 하드코딩된 10개

queuePnrInfos.chunked(10)

제안: 설정 파일로 관리

galileo:
  queue:
    chunk-size: 10
    parallel-size: 5

2. 재시도 로직

현황: 재시도 없음

제안: 제거 실패 시 재시도

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 심층 분석 문서입니다.