오늘 주문하면 오늘 받는 오늘드림 서비스, 이를 위해 뒤에서는 얼마나 바쁘게 돌아갈까요?
안녕하세요. 올리브영 배송 및 물류 스쿼드에서 백엔드 개발을 담당하고 있는 맹곰이입니다.✌️
올리브영은 택배 배송과 오늘드림 두 가지 배송 서비스를 제공하고 있는데요.
이 중 택배 배송과 관련 있는 물류시스템과 데이터 전송 방식은 "올리브영 물류시스템에서는 데이터를 어떻게 주고 받을까?"라는 제목으로 지난 해에 소개드렸습니다.
오늘은 물류시스템만큼 많은 트래픽을 발생하는 오늘드림 서비스에서 외부 배달대행사와 API 통신을 통해서 데이터를 주고 받는 부분을 소개해드리려고 찾아왔습니다! 또한, 배달대행사와 안정적으로 연동하기 위한 API 설계전략과, 실제 운영 중 겪었던 트랜잭션 장애 사례를 공유합니다.
오늘드림
우선 다들 아시겠지만 오늘드림부터 간단히 소개해 드릴게요. 오늘드림을 한줄로 표현해보면, 원하는 상품을 당일에 빠르게 배송 받을 수 있는 즉시배송 서비스입니다.
오늘드림은 빠름 배송 기준, 보통 1시간 이내로 여러분의 집으로 도착을 하는데요. 이렇게 빠르게 배송이 가능한 이유는 전국 여기저기에 올리브영 매장이 여러분 집 주변에 있기도 하지만, 빠르게 배송을 해주는 배달대행사도 한 몫을 하고 있습니다. 오늘은 이 배달대행사와 데이터를 주고 받는 부분을 소개해드리려고 합니다.
시스템 간에 데이터를 주고 받는 방법은 저번 시간에 소개해드린거 처럼 여러가지 방법이 있는데요. 배달대행사와 데이터를 주고 받는 부분은 API를 사용해서 연동을 했습니다.
여러분이 오늘드림으로 주문하면 매장에서 상품 포장이 완료되면 배달대행사를 호출하게 되는데요. 올리브영은 배달대행사와 긴밀한 API 연동을 통해 '오늘드림' 서비스를 운영합니다. 이 연동은 크게 두 단계로 이루어집니다.
- 올리브영 -> 배달대행사 API 호출 : 고객의 주문이 접수되고 매장에서 상품 포장이 완료되면, 올리브영 시스템은 배달대행사 주문 접수 API를 호출하여 배송을 요청합니다.
- 배달대행사 -> 올리브영 API 호출 (콜백) : 배달대행사는 라이더 배차, 상품 픽업, 배송 완료 등 배송상태가 업데이트 될 때마다 올리브영 API를 호출하여 그 정보를 전달합니다.
오늘드림 주문 배달대행사 접수 API
우선 올리브영에서 호출하는 배달대행사 주문 접수 API 연동부터 살펴볼까요? API는 상황에 따라서 여러 문제가 발생할 수 있는데요. Read Timeout, Connection Timeout, 서버 에러, 클라이언트 에러 등 여러 문제가 있습니다. 이 에러들을 재시도하면 성공할 수 있을지 여부에 따라서 나누어서 생각해보려고 합니다.
해당 개념을 쉽게 이해하도록 Read Timeout, Connection Timeout을 간단하게 전화에 비유해서 표현해 볼게요.
- Read Timeout : 전화를 걸어 상대방이 받았으나, 일정 시간 동안 상대방이 응답이 없어 내가 끊어버린 경우
- Connection Timeout : 전화를 걸었으나, 일정 시간동안 상대방이 받지를 않아 내가 끊어버린 경우
Read Timeout, Connection Timeout, 서버 에러
Read Timeout, Connection Timeout 등의 에러는 일시적으로 발생하는 에러로 대부분 잠시 후에 재시도할 경우 에러가 발생하지 않는 케이스입니다. 그렇기에 이러한 케이스는 내부적으로 잠시 후 재시도하도록 처리했습니다.
그러나 여기서 Read Timeout은 중복처리의 문제가 있을 수도 있습니다. 클라이언트 입장에서는 Timeout으로 주문 접수 실패로 판단했지만, 서버인 배달대행사에는 설정된 Timeout값보다 오래 걸렸지만 접수가 처리된 경우도 있죠.
다행히 대부분의 배달대행사에서는 똑같은 주문번호로 접수를 하게 되면 중복접수로 응답을 하는 기능이 있습니다. 중복접수 응답이 오면 내부적으로는 접수성공으로 판단을 하여 해당 문제를 해결하고 있습니다.
그리고 서버 에러의 경우에는 일시적인 에러일수도 있지만, 그렇지 않은 경우도 있습니다. 그렇지 않는 경우라 함은, API를 호출하면 서버에서 지속적으로 에러로 응답할 것이고 될 때까지 기다릴수는 없기에... 재시도 횟수 또한 제한 하고 있습니다.
클라이언트 에러
클라이언트 에러는 대부분 다시 시도를 해도 계속해서 에러가 발생할 텐데요. 이러한 경우에는 주문접수를 할 수가 없고 여러분이 주문한 상품을 배달을 할수가 없습니다. 이러한 상황으로는 대표적으로 주소가 잘못되었거나 배달대행사에서 라이더가 부족하다거나 기상악화로 배달을 못 하는 경우가 있습니다.
아래는 주문접수 API 호출 시 사용하는 코드 샘플이며, 주문접수 성공 시에는 http status가 200으로 응답, 중복접수 시에는 409, 잘못된 요청은 400, 서버 에러는 500으로 응답이 온다를 가정했습니다.
webClient.post()
.uri("URI 주소")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestDto)
.header("Authorization", "인증 값")
.retrieve()
// 접수성공
.onStatus(HttpStatus::is2xxSuccessful) { response ->
response.bodyToMono(ResponseDto::class.java).flatMap { errorBody ->
Mono.empty()
}
}
.onStatus(HttpStatus::isError) { response ->
response.bodyToMono(ResponseDto::class.java).flatMap { errorBody ->
// 중복접수
if (response.statusCode() == HttpStatus.CONFLICT) {
logger.info("중복 접수")
Mono.empty()
} else {
when {
// 클라이언트 에러
response.statusCode().is4xxClientError -> {
logger.info("접수 실패")
Mono.empty()
}
// 서버 에러
else -> {
Mono.error(
WebClientResponseException(
"주문접수 실패",
response.rawStatusCode(),
response.statusCode().reasonPhrase,
response.headers().asHttpHeaders(),
errorBody.toString().toByteArray(),
null
)
)
}
}
}
}
}
.bodyToMono(ResponseDto::class.java)
.block()오늘드림 콜백 API
다음은 올리브영이 제공하는 배달대행사에서 라이더가 배차가 되고, 상품을 픽업, 배송완료 시 호출하는 API입니다. 이러한 콜백 API는 라이더 배차, 상품 픽업, 상품 배송 완료, 배달 취소 등의 기능으로 구성됩니다.
- 라이더 배차 : 라이더가 배차가 되었다고 알려주는 기능입니다.
- 상품 픽업 : 라이더가 상품을 픽업했다고 알려주는 기능입니다.
- 상품 배송완료 : 라이더가 상품을 배달완료했다고 알려주는 기능입니다.
- 배달 취소 : 배달을 수행 할 수 없을 때 알려주는 기능입니다.
이 API들은 얼마전에 대규모 리뉴얼을 진행했습니다.
리뉴얼 하게 된 이유
- 배달대행사마다 같은 기능을 하는 각자 다른 api로 이루어져 있어 하나로 합치자.
리뉴얼을 하게 된 이유는 여러가지가 있지만, 그 중에서도 각기 다른 API로 이루어진 기능을 한 개 API로 통합한 이유는 이렇습니다.
기존에는 제일 처음 도입된 A 배달대행사 전용 콜백 API가 있었습니다. 그리고 B 배달대행사가 추가되면 B 배달대행사에 맞춰서 콜백 API를 개발을 했고, C 배달대행사가 추가되면 C 배달대행사 콜백 API를 개발 했습니다.
이렇게 1개씩 추가되다보니... 동일 기능을 하는 API가 여러개가 되어 관리해야하는 API 숫자가 늘게 되었습니다.
그러다보니 발생하는 문제가 기능 수정 시 여러 API를 수정해야하기에 일부 API에는 누락될 가능성이 많아졌습니다. 또한 내부 로직은 같은 코드 사용하도록 하여 수정은 한곳만 하도록 구현하더라도 테스트는 각각 API를 해야 하기에 시간이 더 많이 필요한 등의 이슈가 있습니다.
이러한 문제점들을 해결하고 관리 효율성을 위해 1개 기능을 하는 API는 1개가 되도록 리뉴얼을 하게 되었습니다!
덕분에 아래 그림처럼 API가 12개에서 4개로 줄어들었고, 운영상에서도 봐야할 지표 및 로그 종료도 그만큼 줄어들어 관리 효율성이 증대되었습니다.
트랜잭션 에피소드
위 그림처럼 배대사 API는 1개로 리뉴얼은 성공적으로 완료되었습니다!! 리뉴얼하면서 발생한 기술적인 에피소드가 하나 있어 추가로 소개해드릴려고 합니다. 개발을 하다보면 많은 에피소드가 있을텐데요. 그중 오늘 소개해드리려는 에피소드로 단 한 줄의 코드 때문에 서버가 멈춰버린 이슈를 풀어보겠습니다.🫢🫣
이는 트랜잭션을 무분별하게 사용하면서 서버에 과부하가 발생한 결과였습니다. 그렇다면 문제의 한 줄이 무슨 내용이었을지 짐작 가시나요?
그 1줄은 UPDATE 쿼리의 트랜잭션 전파옵션입니다.
트랜잭션 전파옵션 간단설명!
- REQUIRED : 기존 트랜잭션과 동일한 트랜잭션을 사용합니다.
- REQUIRES_NEW : 새로운 트랜잭션을 생성합니다.
UPDATE 쿼리를 실행하는 메소드에 트랜잭션 전파옵션을 REQUIRED로 설정했는데, 동일 트랜잭션을 사용할 수 있는 메소드가 있었습니다. 그렇지만 이 메소드의 트랜잭션을 분리해 REQUIRES_NEW로 변경했습니다. 이로 인해 메소드 호출 한 번당 트랜잭션이 기존 대비 2배로 증가하게 되었습니다. 평소에는 동시 트랜잭션 수가 Connection Pool 크기를 넘지 않아 문제가 없었지만, 특정 시간대에 해당 메소드 호출이 급증하면서 상황이 달라졌습니다. Connection Pool 크기를 초과하는 트랜잭션 요청이 발생했고, Connection을 얻지 못한 스레드들이 대기 상태로 쌓이기 시작했습니다. 대기 스레드가 계속 증가하면서 서버에 과부하가 걸리니 결국 서버가 응답하지 않는 상황까지 이어진거죠. (사전에 Connection Pool 용량이 충분한지 계산해봤더라면 이런 상황을 예방할 수 있었을텐데요😭)
다행히 MSA가 적용되어 일시적인 일부 장애로 끝났고 서버 증설로 임시 대응이 가능하였으며, 현재는 트랜잭션 전파옵션을 변경해서 이슈를 해결하였습니다.
마무리
올리브영 오늘드림 주문건수는 계속해서 늘고 있는데요. 그에 비례해서 배달대행사와 연동하는 데이터 양도 많아졌습니다. 또한 이로 인하여 트래픽도 계속해서 늘고 있는데요.
위 사례을 겪으면서 배운 세 가지 교훈을 정리해 볼게요!!
- MSA는 왜 해야하는가 : MSA가 적용되어 있어 올리브영 전체 서비스 장애로 이어지지 않았습니다.
- 트랜잭션 전파옵션의 중요성 : 트랜잭션 전파옵션 1줄로 서버가 멈추는 결과까지 이르게 되었습니다.😭
- 부하 테스트의 중요성 : 부하 테스트 한번 해보았으면 해당 문제는 배포 전에 발견했을 거 같아요.
여러분의 프로젝트에서도 외부 시스템 연동이나 트랜잭션 관리로 고민하고 계신다면, 이 글이 시행착오를 줄이는 데 도움이 되길 바랍니다.
그럼 전 이만 또 다른 기능을 개발하러 가보겠습니다!!!
감사합니다.

