올리브영 테크블로그 포스팅 외부셀러 - 외부 스파크성 트래픽으로부터 내부 시스템을 보호하는 방법 1탄
Backend

외부셀러 - 외부 스파크성 트래픽으로부터 내부 시스템을 보호하는 방법 1탄

비동기(Async) 및 목적별 서비스 분리를 활용하여 스파크성 트래픽 대응

2023.12.15

외부 대용량 트래픽으로부터 내부 시스템을 보호하는 방법 1탄 (총 2편으로 제작됩니다)


  • 안녕하세요 올리브영 에서 외부셀러 시스템을 담당하고있는 윤긱스 입니다.
  • 외부셀러 서비스는 사방넷, 플레이오터, 샵링커 등 쇼핑몰통합관리솔루션 과 내부 올리브영의 API 를 연결해 상품,주문 관리,배송 등 외부 쇼핑몰 통합솔루션 연동을 위한 서비스 입니다.
  • 아래 내용에서는 외부에서 들어오는 사용자의 스파크성 트래픽을 어떤식으로 외부셀러에서는 대비 하고있는지를 소개하려고 합니다.

🧐 기존 직면 했던 과제

  • 쇼핑몰통합관리솔루션이 요청하는 상품,주문,배송 과 관련된 API 요청을 외부셀러서비스는 올리브영 내부 API를 호출하고 응답해주는 동기(Sync) 방식으로 결과를 전달하고 있었습니다.
  • 아래 이미지는 상품 API 를 응답하는 하나의 예제를 시퀀스로 그려보았습니다.

image_1.png

  • 위 지표에도 알 수 있듯이 상품 등록 요청이 올 경우 올리브영 상품 API 내부 서비스에서 validation 및 실제 채번(numbering) 및 등록 과정을 모두 모든 처리를 하기 전까지 셀러 서비스 입장에서는 대기를 하게 되고, 사용자(사방 넷, 숍 링커) 등을 이용하는 사용자는 최종적으로 모든 작업이 끝날 때까지 Waiting 을 해야 하는 문제가 있습니다.
  • 또한 정상 입력된 상품인데도 네트워크 단순, 배포로 인한 오래 걸리는 응답 실패를 사용자에게 전달하고, 안 좋은 경험을 전달하는 현상 또한 확인하였습니다.
  • 아래는 사용자가 동시에 순간적으로 상품 API 입력 시 나타나는 APM 지표의 모습입니다.

image_2.png

  • 위 지표에 알 수 있듯이 내부 Internal 통신에서 대기시간(Latency)이 길게 발생할 경우 (10s 이상) 외부 셀러 서비스 또한 CPU /Memory가 상승하게 됩니다.
  • 현재 동기(Sync)방식에서는 내부서버에서 최종응답을 할때까지 외부셀러 서비스 입장에서 네트워크 요청대기가 늘어나고, 외부와의 직접적인 통신하고있는 외부셀러 서비스 서버는 CPU/ Memory 는 상승및 주문/배송 에대한 타 서비스 이용에도 영향을주게 됩니다.
  • 하지만, 위 문제가 발생하더라도 현재 외부 셀러 서비스 입장에서는 해당 서버를 계속 늘리는 방법 외에는 대체할 수 있는 상황이 아니었습니다.

🤔 문제점 파악 및 해결하고자 하는것

  1. Heavy 한 내부 작업이 동기적인 응답 방식으로 인해 외부 셀러에 발생되는 문제를 해소!!
  2. 정상 상품 요청인데도 갑작스러운 실패 응답은 안 좋은 경험을 유발!! 안 좋은 경험에 대한 해소!!
  3. 갑작스러운 트래픽 다른 주문, 배송 API 영향도 없이 대응에 빠르게 대응!!

👉 기존 현실세계 에서의 사례 찾아보기

image_3.png

  • 커피숍 서비스를 예를 들어 설명하고자 합니다.
    • 일반적인 손님이 적은 커피숍에서는 손님이 오면 한 직원이 상품을 선택한 뒤 주문 및 결제를 하게 됩니다.
    • 한 건 두 건의 주문이 들어오고 제조할 때는 문제가 되지 않지만, 갑자기 많은 손님이 들어오게 된다면
    • 주문 > 제조 > 응답 과정이 완료될 때까지 손님이 입구에서 기다려야 합니다.
    • 또한 손님들은 자신의 커피를 주문하고 받을 수 있는 시간을 예측하지 못합니다.

  • 그렇다면 스마트한 사장님은 어떻게 이 문제를 해결할까요?
    • 사장님은 이러한 상황에 대해 고민합니다. 물론 직원을 무작정 늘리는 방법도 있겠지만,
    • 주문 처리를 담당하는 직원을 늘리거나 주문 대수에 맞춰 대응하는 방법, 또는 생산을 담당하는 직원을 늘려 생산 능력을 향상시키는 방법 중에 어떤 것이 효과적일지를 고민합니다.
    • 즉, 역할을 명확하게 나누고 해당 역할에 전문성을 가진 직원을 늘려 서비스 품질과 효율성을 향상시키는 것이 가장 좋은 해결책이라고 생각될 것입니다.

  • 현대의 대부분의 커피숍은 어떤 모습일까요?
    • 요즘 커피숍에서는 예전과는 다르게 주문 시에 키오스크라 불리는 기계를 자주 볼 수 있습니다.
    • 키오스크는 손님이 주문하려는 메뉴를 선택하고 결제하는 역할을 맡아주며, 손님들은 웨이팅 번호 및 확인증을 받아옴으로써 주문에 대한 대기 시간을 최소화할 수 있습니다.
    • 이후에는 직원들은 키오스크에서 전달받은 이벤트에 따라 제조에만 집중 하여 주문된 순서에 맞게 제품을 생산합니다.
    • 번호가 불리면 손님은 자신의 차례가 얼마나 남았는지 예측할 수 있어 사용자는 효율적인 서비스를 경험할 수 있게 됩니다.

중요한 부분은 필요에 의해 커피숍 사장님은 키오스크를 늘려 주문을 받는 속도를, 생산이 더디면 생산하는 직원을 늘려 제품을 생성하는 속도를 유동적으로 조절할 수 있다는 사실입니다.

✋ 그렇다면! 외부셀러 서비스에 개선방식의 포인트는?

  1. 내부 사정은 내부 사정!! 서비스 이용자에게 빠르게 응답해 보자!!
    • 외부 셀러 상품 등록 요청 시 사용자가 원하는 응답을 빠르게 Response
  2. 같은 목적으로 응집도를 높이고, 관심사가 같은 Process를 묶어 보자!!
    • 요청에 대한 검수 Process / 생산 Process를 서비스로 서버 분리 및 비동기(Async) 이벤트 전달 방식 적용.
  3. 사용자에게 네트워크 문제 등 안 좋은 경험을 줄여보자!!
    • 생산 시 네트워크 지연 문제 or 수단 현상이 발생하더라도 Retry로 재시도로 직 추가.

그래서 아래 그림과 같이 seller 서비스를 역할에 따른 서버 분리와 이벤트를 통해 아키텍처를 변경하였으며,
이벤트 전달자 역할로는 AWS MSK 도입하였습니다.

image_4.png

  • 요청 담당 서비스: seller-external-api
    • 상품 정보를 요청받으면 필요한 필수 Validation만 처리하며, 그 이후에는 내부 상품 서에 그에 채반을 요청하고, 해당 AWS MSK로 이벤트를 전달합니다.
  • 생성 담당 서비스: seller-internal-api
    • 생성 전문을 올리브영 내부 API 전달에 초점을 맞추어 올리브영 내부 API에서 Heavy 한 작업일지라도, 순차적으로 처리되도록 구현하였습니다.
    • Retry 로직을 구현하여 Network 문제 등의 이슈가 발생 시 다시 한번 시도할 수 있도록 아키텍처 설계하였습니다.

🙌 특별하게 기술적으로 공유할 만한 꿀팁이 있다면?

비동기 처리로 분리하고 Topic에 Partition을 N 개로, 그리고 Consumer Server도 N 개로 구성된 상황에서 처리 순서를 보장해야 한다면
아래처럼 기본 ProducerRecord에 topic , message 지정만으로는 문제가 발생할 수 있습니다.

 val producerRecord: ProducerRecord<String, Any> = ProducerRecord<String, Any>(topic, message)
 
  • 기본적으로 Partition Key가 부여되지 않는다면 message를 Topic 내 Partition에 Round Robin으로 분배합니다.
    • 위같이 상황은 서로 다른 Consumer 서버에서 처리될 확률 이 높아집니다.
  • 문제를 해결하기 위해 partitionKey를 지정하여, 동일한 상품은 반드시 동일한 Partition에 적재되도록 조치했습니다.

✌️ 꿀팁 정리

partition key 지정시: key를 hash 하여 특정 partition 으로 보냅니다. topic의 파티션 개수가 변경되지 않는경우 유지됩니다.
partition key 미지정시: Round Robin 알고리즘을 사용 하여 partition 분배

image_5.png

위와 같은 상황 때문에 아래와 같이 ProducerRecord에 상품 ID를 partitionKey로 지정함으로써 같은 상품일 경우 같은 파티션에 적재 되도록 하였습니다.

    private fun publish(topic: String, message: ProductPersistentRequestMessage) {

        val producerRecord: ProducerRecord<String, Any> = ProducerRecord<String, Any>(topic, message.partitionKey, message)

        kafkaTemplate.send(producerRecord).addCallback(
            { result ->
                // 성공 시 실행할 코드
                logger.info("publish success messageId=${message.messageId}")
            },
            { ex ->
                // 실패 시 실행할 코드
                logger.error("publish fail messageId=${message.messageId}", ex)
                throw InterruptedException()
            }
        )
    }

결론! 특정 데이터 대해서 우선순위가 중요하다면 파티션 키를 이용해보는 것도 방법.

👉 아키텍처 개선 결과

  • 비동기(Async) 및 Queue 도입으로 외부에서 상품의 스파이크성 데이터 요청이 입력되어도 올리브영 내부 상품 API에 직접적인 영향을 줄여, 외부 스파이크성 트래픽에 대한 내부 올영서비스 보호!!
  • 서버 분리를 통해 현재 정확하게 어떤 리소스가 더 필요한지 (인입에 대한 리소스 / 생성과정에 대한 리소스) 모니터링이 후 빠르게 대응할 수 있는 아키텍처 돌출!!
  • 내부 서비스 간 통신에서 문제가 생기더라도 내부적으로 빠른 재처리로 로직 도입으로 사용자 사용성 개선!!

😁 마무리

  • 커피숍에 비유하여 외부 스파이크성 트래픽으로부터 내부 시스템을 관심사 분리 및 Queue로 이벤트를 전달하여 운영하는 방식을 소개해 보았습니다.
  • 다음 포스팅 글은 **외부 트래픽으로부터 내부 시스템을 보호하는 방법 2탄 (부제:유량제어)**에 대한 주제로 찾아뵙겠습니다. 감사합니다.

😝 Thanks to

  • 언제나 불철주야 함께하는 리테일플랫폼개발팀 감사합니다.
  • 아키텍처 설계에 도움을 주신 코드다이버님 감사합니다.
  • 아키텍처변경시 빠르게 협업 대응 해주신 상품스쿼드 분들 감사합니다.
외부셀러트래픽리테일플랫폼개발팀
올리브영 테크 블로그 작성 외부셀러 - 외부 스파크성 트래픽으로부터 내부 시스템을 보호하는 방법 1탄
윤긱스 |
Back-end Engineer
개발은 정말 몰라! 알수가 없어?!