티몬 항공서비스개발유닛이 실시간 항공권 검색 서비스를 만들며 다룬 대용량 데이터 처리 이야기다. 한 번의 검색에 1GB가 넘는 데이터를 처리해야 하는 이유부터 GDS 연동과 XML 파싱, 두 단계 가공, 필터링, 카우치베이스 적재까지 순서대로 짚는다. 서비스 오픈 후에 덧붙인 압축과 페이징 조치도 함께 나온다.
핵심 포인트- 좌석 잔여 수와 요금이 실시간으로 바뀌기 때문에 실시간 데이터가 아니면 검색 결과가 틀린다.
- 항공 스케줄과 요금은 GDS라는 중개 업체에서 실시간 API로 받아오며 Sabre와 Amadeus를 연동해 쓴다.
- 한 번의 검색에서 GDS와 주고받는 XML이 1GB를 넘는 경우가 많아 GDS별로 호출 서버와 인스턴스를 분리했다.
- XML 파싱과 가공에는 Jaxb를 쓰고 필드 단위와 패키지 단위 두 방식으로 어댑터를 건다.
- 검색 조건에 따라 스케줄 10만 개와 요금 100만 개가 나올 수 있어 최저가만 남기는 필터링을 건다.
- 카우치베이스는 키 하나에 20MB 제한이 있어 GZIP으로 압축하고 넘치면 페이징해 적재한다.
상세 정리- 실시간이 필요한 이유 1: 예약 가능 좌석 수인 Avail이 실시간으로 바뀌므로 반영이 늦으면 예약할 수 없는 결과를 보여주게 된다.
- 실시간이 필요한 이유 2: 항공사와 여행사가 정책과 수요에 따라 요금을 바꾸므로 지연된 데이터로는 정확한 가격을 낼 수 없다.
- GDS의 존재 이유: 항공사가 각국 고객에게 직접 팔려면 언어와 지역 특성을 다 감당해야 해서 여행사와 연계하는데, 그 중개를 맡는 것이 GDS다.
- 연동 현황: 하나투어·자유투어 계열의 Sabre와 모두투어·노랑풍선 계열의 Amadeus가 연동돼 서비스 중이고 Galileo도 작업 중이라고 밝힌다.
- 데이터 규모: 검색 조건에 따라 다르지만 GDS와의 실시간 API 통신에서 1GB 이상을 처리해야 하는 경우가 많다. 서버를 분리해도 파싱과 가공 후 통합 단계가 남는다.
- 1차 가공의 범위: trim, 특수문자 변환, 특정 필드 앞뒤에 고정 데이터 추가처럼 받은 데이터 자체를 손보는 작업이다.
- 1차 가공 방법 1: 특정 필드는 XmlAdapter를 상속해 unmarshal과 marshal을 재정의한 클래스를 만들고 그 필드에 어노테이션을 붙인다. 요금 필드의 소수점 이하를 잘라내는 어댑터가 예시다.
- 1차 가공 방법 2: 패키지 전체에 적용하려면 package-info에 어노테이션을 달아 해당 패키지의 대상 필드에 어댑터가 걸리게 한다.
- 2차 가공의 범위: 여행사 정보와 스케줄, 요금을 내부 규격에 맞게 맞추고 정합성 조건으로 일부를 걸러낸다.
- 필터링 기준: 같은 조건에서 요금이 유독 비싸거나 한 스케줄에 같은 여행사와 요금 조건이 중복되면 최저가만 남기고 나머지를 무의미한 데이터로 보고 제외한다.
- 통합과 적재: GDS별 결과를 필터링·가공한 뒤 통합하고 검색 조건에 따라 정렬해 최종 결과를 만든다. 검색 조건으로 만든 키에 결과 리스트를 값으로 두는 맵 형태로 카우치베이스에 넣는다.
- 용량 대응: 필터링을 거쳐도 키당 20MB 제한을 넘기는 경우가 많아 GZIP으로 압축하고 그래도 넘치면 페이징해 나눠 담는다. 여러 여행사가 입점하며 데이터가 커져 오픈 이후에 추가한 조치다.
- 개선 방향: 스케줄과 요금 데이터를 분리하는 등 구조를 바꿔 압축 없이 카우치베이스를 쓰는 쪽으로 작업 중이라고 밝힌다.
- 남은 과제: 한 번에 처리할 데이터가 커서 검색 응답이 느린 경우가 있고 서버 간 네트워크 트래픽과 전체 시스템 부하도 크다고 인정한다.
왜 읽나외부 대용량 XML API를 여러 곳에서 받아 합쳐야 하는 구조라면 Jaxb 어댑터 활용부터 캐시 키 용량 한계 대응까지 실제 선택지가 나온다.