티스토리 뷰
안녕하세요. Vertical Engineering 팀의 이지민입니다.
지마켓의 여행 플랫폼은 작년에 포스팅한 오픈마켓에서 여행 플랫폼으로 살아남기에서 소개드린 바 있습니다.
여행 플랫폼은 지마켓의 커머스 시스템과 OTA(온라인 여행사 제휴업체) 서버의 API를 통합하여 서비스를 제공합니다. 이처럼 여러 서버와 의존성이 높아, 서비스의 복잡도가 높습니다.
특히, 실시간 API 연동을 요구하여, 견고하게 API를 호출해야 하는 비즈니스 도메인은 다음과 같습니다.
- 여행 상품 상세 페이지
- 실시간 예약
이번 글에서는 여행 상품 상세 페이지와 실시간 예약의 주요 특징과 고려 사항에 대해 설명하고자 합니다.
(1) 여행 상품 상세 페이지
여행 상품 상세 페이지는 내결함성(fault tolerance)이 높아야 하는 화면입니다. 이러한 요건이 생긴 배경과 개발 고려 사항은 다음과 같습니다.
특징
여행 상품 상세 페이지는 지마켓 여행 플랫폼, 지마켓 커머스 시스템, OTA (온라인 여행사 제휴업체) 서버의 API를 조합하여 기능을 제공합니다. 한 화면에서 사용하는 API의 수만 10개 이상이고, 이를 책임지는 팀과 회사도 다양합니다.
다음은 상품 상세 페이지를 구성하기 위해 관여하는 서버를 추상화한 그림입니다.
여행플랫폼의 API에서 문제가 발생할 경우, 여행팀 내부에서 신속하게 대응할 수 있습니다. 반면, 외부에서 제공받은 API는 직접 제어하기 어렵습니다.
특히 일부 OTA (온라인 여행사 제휴업체)는 타사의 API를 직접 연동하기도 합니다. 이 경우, API 문제 발생 시, 대응이 더 어렵습니다.
하나의 상품 상세 페이지를 완성하는 과정은 여러 팀과 회사의 서버가 협력하는 복잡한 작업입니다. 그래서 다수의 API가 항상 완벽하게 동작하기를 기대하기 어렵습니다.
이러한 특징으로 여행 상품 상세 페이지는 내결함성(fault tolerance)이 높은 시스템을 만들어야 한다는 미션이 있습니다.
fault tolerance: 시스템을 구성하는 부품의 일부에서 결함(fault) 또는 고장(failure)이 발생하여도 정상적 혹은 부분적으로 기능을 수행할 수 있는 시스템
API 연동 고려 사항
(a) 타임아웃 시간을 case by case으로 설정하기
일부 API의 응답 지연이 여행 상품 상세 페이지에 영향을 미치지 않게 하려면, 타임아웃을 적절하게 설정해야 합니다.
이때, 두 가지 특징을 고려하여, API 별로 적절한 타
임아웃을 설정해야 합니다.
- 중요도
- 평균 응답시간
중요도가 낮은 API는타임아웃을 적절하게 설정하여, 응답 지연이 전체로 전파되지 않도록 해야 합니다.
반면, 응답시간이 오래 걸리더라도 중요도가 높은 API은 최대한 기다려야 합니다. 대표적인 예로 가격과 재고 조회 API가 있습니다.
여행 도메인에서 모든 실가격과 재고를 데이터베이스에 적재하는 것은 어렵습니다. 가격과 재고는 “상품 x 옵션 x 연령별 요금제 x 날짜 x 시간” 등등의 조합을 통해 결정됩니다.
모든 조합을 사전에 저장해두는 것은 많은 비용이 들기 때문에 실시간으로 조회하여 연산해야 합니다. 이 경우, 오랜 시간 기다려야 합니다.
(b) fallback: 결함이 발생하지 않은 척하기
특정 API에서 오류가 발생하는 경우, 일시적으로 기능을 제공하지 않는 방법으로 내결함성을 높일 수 있습니다.
예를 들어, 할인된 가격을 다루는 API에서 이슈가 발생하는 경우, 원가를 노출하는 경우가 있습니다.
reslience4j을 사용하면 api 에러에 대한 fallback처리를 손쉽게 할 수 있습니다.
다음은 CircuitBreaker에 fallbackMethod를 적용한 샘플코드입니다.
@CircuitBreaker(name = "discount", fallbackMethod = "getDiscountPriceFallback")
public DiscountPrice getDiscountPrice(String productId, BigDecimal originPrice)
...
}
public DiscountPrice getDiscountPriceFallback(String productId, BigDecimal originPrice, Throwable t) {
return DiscountPrice.builder()
.price(originPrice)
.build();
}
(2) 실시간 예약
실시간 예약 처리는 최종 일관성과 멱등성을 보장해야 합니다. 이러한 요건이 생긴 배경과 개발 고려 사항은 다음과 같습니다.
특징
고객의 결제가 승인되면, 예약을 확정하는 “실시간 예약”이 이루어집니다. “실시간 예약”은 지마켓 예약 플랫폼, 커머스 시스템, OTA (온라인 여행사 제휴업체)가 실예약을 계약하는 과정입니다.
이 과정이 중요한 이유는 크게 2가지입니다.
- 금전 거래와 관련 있다.
- 고객은 비용을 지불하고, 판매자는 대금에 대한 정산을 받는다.
- 실시간 예약이 완료되는 시점부터 취소 위약금이 부과될 수 있다.
- 고객이 예약한 서비스를 확실하게 보장해야 한다.
- 만약 연동이 원활하지 않으면, 고객은 여행지에서 예약한 서비스를 제대로 제공받지 못하게 된다.
각 시스템은 자신만의 고유한 상태를 지닙니다. 그리고 각 상태는 결제, 대금정산, 여행 서비스 제공 등의 기반 데이터로 사용됩니다. 그래서 세 개의 시스템은 일관된 상태로 동기화되어야 합니다.
이러한 특징에 의하여, 두 가지 비기능요건이 요구됩니다.
- 최종일관성 (eventual consistency)
- 일시적으로 상태가 어긋나더라도, 결국 동일한 단계의 상태로 동기화가 되어야 한다.
- 멱등성
- 동일한 연산을 여러 번 수행해도 결과가 동일해야 한다.
API 연동 고려 사항
(a) 상태머신 (state machine) 만들기
상태머신을 만들면, 복잡한 상태 연동 과정이 단순해집니다. 그리고 상태 동기화 함수의 재사용성도 높아집니다.
상태머신을 설계할 때에는 다음 두 가지가 사전에 정의되어야 합니다.
- 각 시스템 별 상태 정의하기
- 시스템 간 동일한 단계 매칭하기
다음 그림은 여행 플랫폼, 지마켓 커머스 시스템, 제휴사 시스템의 각 예약과 주문 상태에 대해 정의한 상태 다이어그램입니다.
여행플랫폼에서는 이처럼 상태의 흐름과 규칙을 사전에 정의한 후, 상태를 전이시키는 상태머신을 구현하였습니다.
(b) 대사배치 만들기
일시적인 장애로 인하여, 상태 연동 처리에 실패하는 경우가 있습니다.
이때, 각 시스템의 예약 상태를 확인하고 재연동하는 대사 작업을 통해 이를 보강할 수 있습니다.
오케스트레이터 역할을 하는 배치 프로그램이 각 시스템의 상태를 확인합니다.
그리고 상태가 어긋난 경우, 상태머신을 통해 상태를 재연동합니다.
이를 통해 상태가 일시적으로 어긋나더라도, 결국 일치하게 되는 최종일관성을 얻을 수 있습니다.
(c) 멱등성을 고려하여, 재시도하기
예약 상태 연동 과정에서 API 호출 에러가 발생하는 경우, 재시도로 문제를 해결할 수 있습니다. 그러나 예약 플랫폼에서는 에러응답을 받았으나, 연계 시스템에서는 정상적으로 접수된 상태일 수도 있습니다.
예를 들어, 타임아웃 및 네트워크 오류에 의하여 클라이언트는 에러가 났지만, 서버의 트랜잭션은 정상 종료가 된 경우가 그러합니다. 이 경우, API 호출을 재시도하면 연계 시스템 입장에서는 중복 요청이 됩니다.
중복 요청 시, 연계시스템에서 에러를 리턴하는 경우가 있습니다. 이때 예약 플랫폼은 요청이 정상으로 접수된 것으로 인지할 수 있게 예외처리를 해야 합니다.
(c-2) 중복 요청 시, 명시적인 에러코드가 반환되는 경우
중복 요청 에러 코드가 내려오는 경우는 해피한 케이스입니다.
해당 에러 코드가 내려오면 예약 플랫폼은 이를 성공으로 간주합니다.
(c-2) 명시적인 에러코드는 없지만, 상태 조회 API를 제공하는 경우
때로는 중복요청으로 에러가 발생하더라도, 에러코드로 이를 식별할 수 없는 경우가 있습니다. 이 경우, 연계 시스템의 상태 조회 API를 활용하여 중복 호출 가능 여부를 판단할 수 있습니다.
때로는 연계 시스템의 상태가 다음 상태로 전이될 때까지 기다려야 하는 경우도 있습니다.
이때에는spring-retry의 exception, backoff 옵션을 사용하여, 일정시간 대기 이후 재시도를 할 수 있습니다.
@Override
@Retryable(value = NotReadyReservationException.class, backoff = @Backoff(delay = 5000), maxAttempts = 3)
public void reserve(String id) {
if(!isReservationReady(id)){
throw new NotReadyReservationException(id);
}
...
}
(c) 보상 트랜잭션: 재시도로 해결할 수 없다면 깔끔하게 놓아주자.
재시도로 해결할 수 없는 문제도 있습니다.
- 중복 호출 횟수만큼 실 데이터가 중복으로 접수되는 경우
- 배치에서 일정 기간 동안 재시도를 했음에도 해결되지 않는 경우
이 경우, 보상 트랜잭션을 통해 데이터를 원상태로 돌려주어야 합니다.
보상 트랜잭션을 매끄럽게 처리하려면, 롤백을 하기 쉬운 순서대로 외부 시스템을 연동해야 합니다. 제어하기 어렵고, 금전 거래와 관련된 트랜잭션은 가장 나중에 호출하는 것이 좋습니다.
예를 들어, OTA (온라인 여행 제휴업체)와 실예약을 계약하는 시점부터 취소가 불가능하거나 취소위약금이 부과되는 경우가 있습니다.
만약 OTA (온라인 여행 제휴업체)와 실예약을 한 이후, 지마켓 내부 시스템 연동에 실패한다면 상황을 되돌리기 어렵습니다.
그래서 이 경우, 상대적으로 대응이 쉬운 시스템을 먼저 연동하는 것이 좋습니다.
그리고 돌이킬 수 없는 요청은 최하단에 배치하는 것이 좋습니다.
마치며
본 글을 통해 여러 시스템과 API를 연동하며, 고려했던 점들을 공유드렸습니다. 본 글이 API 연동 과정에서 어려움을 겪는 분들께 도움이 되었으면 합니다.
참고
[도서] 이벤트 기반 마이크로서비스 구축: 8장. 마이크로서비스 워크플로 구축
[아티클] Four Key Differences Between “Online Travel” and “Online Shopping” E-commerce
'Backend' 카테고리의 다른 글
신규 서비스 "꿀템"을 만들기 위한 여정(네? 다음달까지요?) -1편 (13) | 2024.06.30 |
---|---|
조회 속도 개선하기 (ESM '문의하기' 기능 개편) (17) | 2024.06.28 |
Gmarket Mobile Web Vip 악성 봇 대침투 사건 (3) | 2024.05.11 |
설계란 고민의 연속이다 2편 (1) | 2024.04.04 |
설계란 고민의 연속이다 1편 (1) | 2024.03.14 |