시스템 아키텍처 개관
arch-overview pattern-strategy config-infra
이 노트의 목적
air-intl-adapter는 11개 항공 공급사(GDS/NDC/LCC)의 제각각인 API를 하나의 Triple 내부 표준 API로 변환(adapt) 하는 Kotlin/Spring Boot 마이크로서비스다. 이름 그대로 “어댑터(Adapter)” 역할이 본질이다. 이 문서는 신입이 코드베이스에 처음 들어왔을 때 “어디에 무엇이 있고, 요청이 어떻게 흐르며, 왜 이렇게 설계됐는가”를 한 번에 파악하도록 돕는 출발점이다.
1. 한눈에 보는 정체성
| 항목 | 내용 | 근거 (파일) |
|---|---|---|
| 서비스 성격 | 국제선 항공 멀티 공급사 어댑터 마이크로서비스 | CLAUDE.md, AirIntlAdapterApplication.kt |
| 빌드 도구 | Gradle (Kotlin DSL) | build.gradle.kts |
| 언어/런타임 | Kotlin 2.0.0, Java 21 (JvmTarget.JVM_21) | build.gradle.kts:13-24,101-110 |
| 프레임워크 | Spring Boot 3.3.0 (+ spring-boot-starter-web, -web-services, -aop) | build.gradle.kts:14-49 |
| 그룹/패키지 | com.triple.air.intl / com.triple.air.intl.adapter | build.gradle.kts:20 |
| 진입점 | AirIntlAdapterApplication.kt (서버 포트 8000) | AirIntlAdapterApplication.kt, application.yml:5-6 |
| HTTP 노출 | 공급사별 내부 REST API /internals/{SUPPLIER}/... + /health | 각 *Controller.kt, HealthController.kt |
"중앙 디스패처가 없다"
많은 어댑터 시스템은
POST /search하나에supplier파라미터를 받아 분기하는 중앙 디스패처를 둔다. 이 시스템은 그렇지 않다. 공급사마다 자체 REST 컨트롤러(/internals/AMADEUS/search,/internals/SABRE/search…)가 독립적으로 존재하고, 라우팅은 URL 경로(공급사명)로 끝난다. Triple 예약 시스템(호출자)이 “어느 공급사를 부를지” 이미 알고 그 경로로 직접 호출한다. → 자세한 호출 관계는 caller-callee-map 참고.
2. 기술 스택 (build.gradle.kts 기준)
// build.gradle.kts:14-18
id("org.springframework.boot") version "3.3.0"
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
java.sourceCompatibility = JavaVersion.VERSION_21| 카테고리 | 라이브러리 / 버전 | 용도 | 근거 |
|---|---|---|---|
| 웹 | spring-boot-starter-web, -web-services | REST 컨트롤러 + SOAP(WS) 통신 | build.gradle.kts:34-36 |
| 캐시/세션 | Redis + Redisson 3.39.0 (lettuce/jedis 제외) | 검색결과 캐시, 분산락, 세션 | build.gradle.kts:37-44 |
| HTTP 클라이언트 | OkHttp 4.9.3 | 모든 공급사 외부 호출의 실제 전송 계층 | build.gradle.kts:91, support/web/ClientSupport.kt |
| 회복탄력성 | Resilience4j 2.2.0 (spring-boot3 + all) | 서킷브레이커/리트라이/벌크헤드 | build.gradle.kts:78-80 |
| 리트라이(보조) | spring-retry (@EnableRetry) | @Retryable 백오프 | build.gradle.kts:38, AirIntlAdapterApplication.kt:8 |
| 직렬화 | Jackson (kotlin / xml(dataformat) / no-ctor-deser) | JSON + XML/SOAP 양쪽 매핑 | build.gradle.kts:51-55 |
| SOAP 보안 | wss4j 1.6.19 / wss4j-ws-security-dom 2.4.1 | WS-Security 서명/헤더 | build.gradle.kts:46-47 |
| 비동기 | kotlinx-coroutines-core + -slf4j | 병렬 호출, MDC 전파 | build.gradle.kts:58-59 |
| 모니터링 | Datadog APM(dd-trace-api 1.2.0) + opentracing | 분산 추적, traceId | build.gradle.kts:70-76 |
| 오류 추적 | Sentry 6.30.0 (spring-boot-starter-jakarta) | 예외 캡처/알림 | build.gradle.kts:61-62 |
| 비밀관리 | AWS Secrets Manager + S3 SDK | 자격증명 주입, 파일 저장 | build.gradle.kts:64-65 |
| 경보 | Slack API 1.44.2 | 운영/긴급 채널 알림 | build.gradle.kts:82-86 |
| 압축 | snappy-java, brotli/dec, (gzip/deflate JDK) | Redis 값 압축 + 응답 압축 해제 | build.gradle.kts:39,92 |
| LCC 암호화 | libs/TwaySEED.jar (로컬 jar) | T’way SEED 인증 암호화 | build.gradle.kts:57 |
| SFTP | jcraft jsch 0.1.55 | T’way/Jin Air FTP 연동 | build.gradle.kts:82 |
신입이 먼저 외울 4개
OkHttp(나가는 호출) · Redisson/Redis(상태·캐시) · Resilience4j(장애 격리) · Datadog/Sentry(관측). 이 4개가 모든 공급사 모듈을 가로지르는 공통 토대다. 자세히는 configuration-and-infra, resilience-and-events.
3. 패키지 레이아웃 (레이어드 + 멀티공급사)
루트 패키지: com.triple.air.intl.adapter. 최상위는 공통(cross-cutting) 레이어 8개 와 공급사 모듈 묶음 supplier/ 로 나뉜다.
com.triple.air.intl.adapter
├── AirIntlAdapterApplication.kt # @SpringBootApplication, @EnableRetry, JVM 기본 TZ=UTC
│
├── interfaces/ # (공통) REST 진입. HealthController, 공통 request/response DTO, AirportService 등
├── application/ # (공통) 횡단 서비스: SlackService(경보), CalculateTimezoneService
├── configuration/ # Spring 설정: OkHttp, Redis/Redisson, S3, OpenApi, WebMvc(필터순서), Properties
├── domain/ # 공통 도메인: FareRule, Amenity + repository(Redis 기반)
├── infrastructure/ # 공통 외부연동: city(CityClient), slack(SlackClient)
├── support/ # 유틸/공통: web(ClientSupport·MDCHolder·Result), exception, util, enums, cache, filter, log
│
└── supplier/ # ★ 11개 공급사 모듈. 각 모듈이 같은 5+1 레이어를 반복
├── amadeus/ sabre/ galileo/ amadeusndc/
├── tway/ jinair/ koreanair/ lufthansa/
└── singaporeair/ jejuair/ groupair/
3.1 공급사 모듈의 내부 구조 (11개 모두 동일 — “전략 패턴의 손맛”)
각 supplier/{name}/ 은 동일한 6개 하위 패키지를 갖는다 (Glob로 11개 전부 확인됨):
supplier/{name}/
├── interfaces/ # controller/internals/{Name}{Op}Controller.kt + DTO(request/response)
├── application/ # {Name}{Op}Service.kt — 비즈니스 오케스트레이션
├── infrastructure/ # {Name}Client.kt — 실제 외부 API 호출 (OkHttp/SOAP/NDC)
├── domain/ # 공급사 고유 도메인 모델 + repository
├── support/ # 공급사 전용 유틸/enum/model
└── configuration/ # 공급사 전용 Bean/Redis 설정
네이밍 규칙(코드 전반에서 강제됨)
- 컨트롤러:
{Name}{Op}Controller→ 예:AmadeusSearchController,SabreBookingController- 서비스:
{Name}{Op}Service→ 예:AmadeusFlightSearchService,AmadeusTicketingService- 클라이언트:
{Name}Client→ 예:TwayClient,KoreanairClient,GalileoClient- HTTP 경로:
/internals/{SUPPLIER}/{resource}(대문자 공급사명)이 규칙 덕분에 “어느 파일을 열어야 하는지”가 클래스명만 봐도 예측된다. 새 공급사 추가도 이 골격을 복제하는 식이다.
모듈 크기는 균등하지 않다 (kt 파일 수)
공급사 kt 파일 수 공급사 kt 파일 수 sabre 882 koreanair 213 amadeus 873 jinair 114 galileo 525 jejuair 99 amadeusndc 357 singaporeair 88 tway 253 lufthansa 87 groupair 34 GDS(amadeus/sabre/galileo)와 NDC(amadeusndc)는 자동생성된 SOAP/NDC 스키마 DTO가 대부분이라 파일 수가 폭발한다. 신입이 sabre/amadeus를 처음 열면 압도되기 쉽다 → 컨트롤러 → 서비스 → 클라이언트 3개만 따라가면 흐름은 동일하다. groupair(34개)가 골격 학습용으로 가장 적합하다.
4. 공급사 11개 × 통합 방식 매핑
| # | 공급사 | 코드 식별자 | 통합 방식 | GDS 코드 | 주요 오퍼레이션 | 핵심 특이점 |
|---|---|---|---|---|---|---|
| 1 | Amadeus | AMADEUS | GDS / SOAP (TOPAS·ART) | 1A | Search·Booking·Ticketing·FareRule·Queue·CashReceipt | stateful PNR 세션, 가장 큰 모듈, 국내 TOPAS 경유 |
| 2 | Sabre | SABRE | GDS / SOAP(+REST) | 1S | Search·Booking·Ticketing·FareRule·Queue·CashReceipt | 코루틴 사용(SabrePassengerService), SOAP+REST 혼용 |
| 3 | Galileo | GALILEO | GDS / SOAP (Universal API) | 1G | Search·Booking·Ticketing·FareRule·Queue·CashReceipt | SessionContext, 스키마 방대, REST/KPS/KRT 보조 |
| 4 | Amadeus NDC | AMADEUSNDC | NDC (ART) | — | Search·Booking·Ticketing·FareRule·Queue | NDC ART 클라이언트, GPS 보조 |
| 5 | T’way | TWAY | LCC / REST (XML) | — | Search·Booking·Ticketing·FareRule·Ancillary·AgencyCredit | TwaySEED.jar SEED 암호화 인증, FTP |
| 6 | Jin Air | JINAIR | LCC / REST | — | Search·Booking·Ticketing·FareRule·Ancillary·AgencyCredit | 부가서비스/대리점 크레딧, FTP, payment/dsr 분리 |
| 7 | Korean Air | KOREANAIR | NDC (V21.3) | — | Search·Booking·Ticketing·FareRule | NDC 표준, NicePay 결제 |
| 8 | Lufthansa | LUFTHANSA | NDC (V17.2) | — | Search·Booking·Ticketing·FareRule·Ancillary | certification 스키마, 재발행 |
| 9 | Singapore Air | SINGAPOREAIR | NDC (EDIST 18.1) | — | Search·Booking·Ticketing·FareRule·Ancillary | EDIST 스키마, 재발행 샘플 |
| 10 | Jeju Air | JEJUAIR | LCC / REST | — | Search·Booking·Ticketing·FareRule | 취소 코루틴 흔적 |
| 11 | Group Air | GROUPAIR | 그룹/단체 운임 | — | Search·Booking·Ticketing·FareRule | 가장 작은 모듈, 단체 예약, search/booking 엔드포인트 분리 |
근거: support/enums/Supplier.kt(enum 11개), configuration/Properties.kt(공급사별 @ConfigurationProperties), application.yml:11-20(공급사 yml import), 각 supplier/*/interfaces/controller/internals/*Controller.kt(경로 확인).
통합 방식은 크게 3+1 갈래
- GDS/SOAP (Amadeus·Sabre·Galileo): 전통 GDS, XML SOAP, 세션·인증 상태 보유 → amadeus-protocol, sabre-protocol, galileo-protocol
- NDC (Amadeus NDC·Korean Air·Lufthansa·Singapore Air): IATA NDC 표준, 항공사 직접 연결, 버전별 스키마 상이 → koreanair-protocol, lufthansa-protocol, singaporeair-protocol
- LCC/REST (T’way·Jin Air·Jeju Air): 저비용항공사 자체 REST, 공급사별 인증(SEED 등) → tway-protocol, jinair-protocol, jejuair-protocol
- 그룹 (Group Air): 단체 운임 전용 → groupair-protocol
각 공급사 개요는: amadeus-overview · sabre-overview · galileo-overview · amadeusndc-overview · tway-overview · jinair-overview · koreanair-overview · lufthansa-overview · singaporeair-overview · jejuair-overview · groupair-overview.
5. 레이어 경계 다이어그램
flowchart TD Caller["Triple 예약 시스템 외부 호출자<br/>HTTP POST 또는 GET<br/>헤더 x-triple-sales-channel, x-triple-sales-funnel, PNR 등"] Caller -->|"/internals/SUPPLIER/resource"| Filters subgraph Filters["서블릿 필터 체인 WebMvcConfig 순서"] F1["Order 1 ContentCachingWrapperFilter<br/>요청 및 응답 본문 캐싱"] F2["Order 2 MDCFilter<br/>헤더에서 MDC로 channel, funnel, pnr, traceId"] F3["Order 3 AdapterLoggingFilter<br/>구조화 로깅"] F1 --> F2 --> F3 end Filters --> Interfaces subgraph Interfaces["INTERFACES supplier 내부 interfaces/controller/internals"] I["Name+Op Controller<br/>@RestController @RequestMapping /internals<br/>@CircuitBreaker name은 supplierSearch, fallbackMethod 지정 Resilience4j<br/>요청 DTO 검증, CacheKeyGenerator, 서비스 호출, View 변환"] end Interfaces --> Application subgraph Application["APPLICATION supplier 내부 application"] A["Name+Op Service<br/>비즈니스 오케스트레이션 여러 Client 조합<br/>캐시 read 및 write, 병렬화 coroutine pmap<br/>예외를 공통 ErrorMessage로 매핑"] end Application --> Infrastructure subgraph Infrastructure["INFRASTRUCTURE supplier 내부 infrastructure"] N["Name Client extends ClientSupport<br/>실제 외부 호출 OkHttp, SOAP NDC REST<br/>LoggingAndCompressionInterceptor 요청 응답 로그 + gzip br deflate 해제"] end Infrastructure --> Ext["외부 공급사 API<br/>GDS NDC LCC"] Infrastructure --> Redis["Redis Redisson<br/>캐시, 세션, 분산락"] Infrastructure --> Obs["공통 서비스 및 관측<br/>SlackService 경보, Datadog, Sentry"] Support["support 횡단 토대<br/>web, exception, util, enums, cache<br/>application/SlackService<br/>모든 레이어가 공유하며 역방향 의존 없음"] Support -.-> Interfaces Support -.-> Application Support -.-> Infrastructure
- 외부 호출자(Triple 예약 시스템)는 헤더
x-triple-sales-channel,x-triple-sales-funnel, PNR 등을 실어/internals/{SUPPLIER}/{resource}경로로 HTTP POST/GET 호출 - 서블릿 필터 체인(WebMvcConfig 순서):
[@Order 1] ContentCachingWrapperFilter(요청/응답 본문 캐싱) →[@Order 2] MDCFilter(헤더→MDC: channel/funnel/pnr/traceId) →[@Order 3] AdapterLoggingFilter(구조화 로깅) - INTERFACES:
{Name}{Op}Controller에@RestController @RequestMapping(/internals/..),@CircuitBreaker(name="{n}Search", fallbackMethod=...)(Resilience4j) 부착, 요청 DTO 검증 → CacheKeyGenerator → 서비스 호출 → View 변환 - APPLICATION:
{Name}{Op}Service가 비즈니스 오케스트레이션(여러 Client 조합), 캐시 read/write, 병렬화(coroutine pmap), 예외→공통 ErrorMessage 매핑 - INFRASTRUCTURE:
{Name}Client : ClientSupport가 실제 외부 호출(OkHttp, SOAP/NDC/REST),LoggingAndCompressionInterceptor가 요청/응답 로그 + gzip/br/deflate 해제 - 하단 3대 종착지: 외부 공급사 API(GDS/NDC/LCC) · Redis(Redisson, 캐시·세션·분산락) · 공통 서비스/관측(SlackService 경보·Datadog·Sentry)
support/(web·exception·util·enums·cache)와application/SlackService는 모든 레이어가 공유하는 횡단 토대(역방향 의존 없음)
5.1 레이어별 책임 요약
| 레이어 | 패키지 | 책임 | 대표 클래스 (file:line) |
|---|---|---|---|
| Interfaces | interfaces, supplier/*/interfaces | HTTP 수신·DTO 검증·View 변환·서킷브레이커 부착 | AmadeusSearchController.kt:18-26 |
| Application | application, supplier/*/application | 오케스트레이션·캐시·병렬화·에러 매핑 | AmadeusFlightSearchService.kt, SabrePassengerService.kt:8 |
| Infrastructure | infrastructure, supplier/*/infrastructure | 외부 API 전송(OkHttp/SOAP/NDC) | TwayClient.kt, support/web/ClientSupport.kt:25 |
| Domain | domain, supplier/*/domain | 도메인 모델·Redis repository | domain/FareRule.kt, domain/repository/* |
| Support(횡단) | support/* | web·exception·util·enums·cache·filter·log | MDCHolder.kt, ClientSupport.kt, ErrorMessage.kt |
의존 방향 규칙
interfaces → application → infrastructure → (외부)로 한 방향으로 흐른다.support는 모든 레이어가 의존하는 공통 기반(역방향 의존 없음). 신입이 코드를 읽을 때는 항상 컨트롤러에서 시작해 이 방향을 따라 내려가면 길을 잃지 않는다. 실제 요청 흐름은 request-flow에서 단계별로 추적한다.
6. 진입점과 부트스트랩에서 알아야 할 디테일
// AirIntlAdapterApplication.kt
@EnableRetry
@SpringBootApplication
class AirIntlAdapterApplication {
@PostConstruct
fun init() = TimeZone.setDefault(TimeZone.getTimeZone("UTC")) // ★ JVM 기본 시간대 UTC 고정
}함정 1 — JVM 전역 시간대를 UTC로 강제한다
@PostConstruct에서TimeZone.setDefault(UTC)를 호출한다. 국제선은 출발/도착 공항이 서로 다른 시간대이므로, 코드 전반의LocalDateTime/LocalDate는 암묵적으로 UTC 기준이라고 가정하고 읽어야 한다. 로컬에서now()를 KST로 기대하면 디버깅이 어긋난다. 시간대 변환은 별도 서비스application/CalculateTimezoneService.kt가 담당.
부트스트랩 설정 핵심:
- 프로파일:
local / dev / qa / staging / prod / worker(configuration/Profiles.kt).worker는 배치/큐 처리용으로 항상 다른 프로파일과 함께 활성화된다(SlackService.kt:18-21에서worker제외 후 첫 프로파일을 환경명으로 사용). - 공급사 설정 분리 import:
application.yml:9-20에서classpath:supplier/{name}.yml10개를 import (groupair 포함, NDC인 amadeusndc는 amadeus.yml에 포함). 비밀값은 prod에서optional:aws-secretsmanager:prod/air-intl-adapter로 주입(application-prod.yml:6-7). - 가상 스레드 ON:
spring.threads.virtual.enabled: true(application.yml:23-25) — Java 21 가상 스레드로 블로킹 외부 호출을 다수 처리. - 에러 자동설정 제외:
ErrorMvcAutoConfiguration제외(application.yml:22) → 오류 응답은 전적으로RestExceptionHandler(@ControllerAdvice)가 책임. → error-handling.
7. “이벤트/상태 전파”는 메시지큐가 아니다
이 시스템에 Kafka/RabbitMQ 같은 메시지큐는 없다
분산 상태 전파는 다음 3가지로 구현된다:
- Resilience4j 서킷브레이커 상태전이 —
@CircuitBreaker(name="amadeusSearch")가 실패율 35% 초과 시 OPEN으로 전이,searchFallback이 빈 결과 반환 + Datadog 스팬에supplier.circuit-breaker=OPEN태그(AmadeusSearchController.kt:24,72-80). 설정은application.yml:37-70(slidingWindow 180s/TIME_BASED, failureRate 35%, open 120s).- 예외 전파 + Sentry/Datadog — 모든 예외는
findRootCause()→log()→sentryLog()로 캡처되고 MDC 컨텍스트(traceId 등)가 태그로 부착됨(RestExceptionHandler.kt:77-105). 코루틴 예외는AdapterCoroutineExceptionHandler가 동일 핸들러로 위임.- Slack 경보 — 발권 실패/취소 실패/QUOTA 부족/타임아웃 등 운영 개입이 필요한 사건을
application/SlackService.kt가 긴급/운영 채널로 전송. 메시지에 채널·공급사·PNR·TraceId가 포함된다.자세히는 resilience-and-events, 비동기 처리는 async-coroutines.
8. 신입이 알아야 할 “큰 그림” 5가지
멘탈 모델
- 나는 변환기다. Triple이 “이 공급사로 검색해줘”라고 표준 요청을 보내면, 나는 그것을 공급사 고유 포맷(SOAP/NDC/REST)으로 바꿔 호출하고, 응답을 다시 Triple 표준 View로 바꿔 돌려준다. 비즈니스 “결정”(어느 공급사를 쓸지)은 호출자가 한다.
- 모든 공급사는 같은 골격을 반복한다. Controller → Service → Client. 하나를 이해하면 11개를 이해한 셈. 단, 외부 프로토콜만 다르다.
- 상태는 Redis에 산다. 검색결과·세션·분산락. 어댑터 자신은 가능한 한 stateless를 지향하지만 GDS 세션(Amadeus PNR)처럼 stateful한 부분이 함정이다.
- 장애는 격리한다. 한 공급사가 느려지거나 죽어도 서킷브레이커가 끊고 빈 결과를 주어 전체 검색이 멈추지 않게 한다.
- 운영자가 본다. 자동 처리가 실패하면 Slack으로 사람에게 넘긴다(수동 취소/환불/VOID). 코드의
slackService.sendXxxFail(...)호출은 “여기서 자동화가 끝나고 사람이 개입한다”는 신호다.
학습 순서 추천
- [지금 문서] system-architecture
- → request-flow — 요청 한 건이 필터 → 컨트롤러 → 서비스 → 클라이언트를 어떻게 통과하는가
- → caller-callee-map — 누가 이 어댑터를 부르고, 이 어댑터는 누구를 부르는가
- → common-operations — Search/Booking/Ticketing/… 6대 오퍼레이션의 공통 의미
- → groupair-overview — 가장 작은 모듈로 골격 체득 → 이후 원하는 공급사로 확장
전체 온보딩 동선은 onboarding-map, 빠른 참조는 quick-reference, 함정 모음은 landmines에서.
9. 핵심 진입 파일 빠른 참조
| 무엇을 보려면 | 파일 |
|---|---|
| 앱 부트스트랩/TZ | AirIntlAdapterApplication.kt |
| 의존성/스택 | build.gradle.kts |
| 전역 설정·서킷브레이커·공급사 import | src/main/resources/application.yml |
| 공급사별 자격증명 구조 | configuration/Properties.kt |
| 필터 순서·ObjectMapper | configuration/WebMvcConfiguration.kt |
| 외부 호출 공통 추상 | support/web/ClientSupport.kt |
| 요청 컨텍스트(MDC) | support/web/MDCHolder.kt |
| 전역 예외 처리 | support/exception/RestExceptionHandler.kt |
| 운영 경보 | application/SlackService.kt |
| 비동기 헬퍼 | support/util/CoroutineExtensions.kt |
| 컨트롤러 예시 | supplier/amadeus/interfaces/controller/internals/AmadeusSearchController.kt |
기존 심층 분석 문서(공급사 외부 API 관점)
amadeus-gds · sabre-gds · galileo-gds · singaporeair-ndc