[배민스토어] 배민스토어에 이벤트 기반 아키텍처를 곁들인…

Aug.17.2023 김민태

Backend

배민스토어는 뷰티, 가전, 책, 패션, 꽃, 반찬 등 다양한 카테고리의 상품을 제공하는 서비스입니다.

지난 배민스토어 글 두 편에서 배민스토어 서비스와 개발 과정 일부를 자세히 소개해 드렸는데요.
이번 글에서는 배민스토어 전시 영역의 아키텍처를 간단히 소개하고자 합니다.

이전 글 참고

시작하기에 앞서, 대표로 전시 아키텍처 글을 쓰게해주신 배민스토어서비스개발팀 여러분들께 감사를 표합니다..!!

(애니메이션 – 카이지 캡처)
억지로 쓴거 아님, 진짜 아님

배민스토어서비스개발팀은 어떤 일을 하는지

점점 더 빠르게 진화하고 있는 커머스 시장에서는 이제 흔하게 생각할 수 있는 새벽 배송을 넘어서 퀵커머스 서비스를 제공하는 배민스토어는 현재 뷰티, 가전, 책, 패션, 꽃, 반찬 등 다양한 카테고리의 상품을 제공하고 있습니다. 이에 맞춰 배민스토어서비스개발팀에서는 다음과 같은 일을 진행하고 있습니다.

  • 상품정보를 등록하고 상품 정보를 사용자에게 보여주는 일
  • 주변의 지점 정보를 빠르게 사용자에게 전달해 주는 일
  • 쿠폰, 할인 혜택을 사용자에게 보여주는 일
  • 이외

배민스토어서비스개발팀에서는 현재 장바구니 직전까지의 모든 시스템을 개발하고 있습니다.

배민스토어의 MSA

우아한형제들의 많은 서비스들은 MicroService Architecture(이하 MSA) 구조로 구현되고 있습니다. 이제는 서비스의 단위를 작게 도메인별로 시스템을 구현하는 방식으로 대다수의 기업들이 시스템 아키텍처를 구현하고 있습니다. 배민스토어 또한 다양한 사외/사내 도메인들을 연결을 통하여 서비스를 만들어 나아가고 있습니다.

다양한 도메인 간의 많은 통신이 일어나는 시점에서 배민스토어의 시스템을 안정적으로 구현하고, 응답 속도, 컴퓨팅 리소스를 효율적으로 활용하기 위하여 다양한 기술을 사용하고 있습니다. 이 중에서 서비스 간의 통신을 효율적으로 하기 위하여 Kafka를 사용하고 있으며 DynamoDB, Redis를 활용하여 배민스토어의 상품을 빠르게 노출하기 위한 노력을 하고 있습니다.

기술 스택

  • Kotlin + Spring WebFlux
  • Amazon DynamoDB
  • Redis
  • Airflow
  • Kafka
  • 이외

다양한 기술 스택을 갖고 어떠한 도메인을 연결하고 있는지 알아보겠습니다. 먼저, 배민스토어는 사용자가 상품 정보를 확인할 수 있도록 다음과 같은 정보를 노출하고 있습니다.

  • 상품 데이터
  • 리뷰 평점 및 리뷰 데이터
  • 지역별 가게 정보
  • 쿠폰 데이터
  • 이외

사용자가 배민스토어를 이용하여 상품을 검색하고 검색된 상품을 고를 수 있도록 상품에 대한 정보를 배민스토어를 이용하시는 분들에게 빠르고 정확하게 노출해야합니다.

자연스럽게 다른 도메인들과 데이터를 주고받으며 안정적인 서비스를 구현해야 합니다. 일반적으로 MSA 구조에서는 HTTP API를 활용하여 다른 도메인들과 데이터를 주고받습니다. 하지만 이러한 동기적 MSA 구조에서는 다른 도메인의 시스템의 상태에 영향을 직접적으로 영향을 끼칩니다.

전시 도메인

전시 도메인의 경우 일반적으로 데이터가 변경되는 시점에서 사용자들에게 바로 노출하는 것을 중요하게 여깁니다. 그리고 트랜잭션을 보장이 필요한 로직도 많지 않습니다. 이러한 도메인 특성을 감안하는 것이 배민스토어 개편 당시 시스템 아키텍처를 팀에서 논의할 때 ReadModel, WriteModel을 분리하여 ReadModel을 전시 영역에 알맞게 최적화하기로 했습니다. 당시 참고했던 모델은 CQRS 패턴이며 잘 설명하고 있는 문서로 정의를 가져와 보았습니다.

CQRS 패턴

– CQRS는 데이터 저장소에 대한 읽기 및 업데이트 작업을 구분하는 패턴인 명령과 쿼리의 역할 분리를 의미합니다.
– 애플리케이션에서 CQRS를 구현하면 성능, 확장성 및 보안을 최대화할 수 있습니다.
– CQRS로 마이그레이션하면 유연성이 생기므로 시스템이 점점 진화하고 업데이트 명령이 도메인 수준에서 병합 충돌을 일으키지 않도록 할 수 있습니다.

(출처: https://learn.microsoft.com/ko-kr/azure/architecture/patterns/cqrs)

CQRS 패턴의 핵심은 데이터 저장소에 대한 읽기 작업과 쓰기 작업을 별도의 역할로 분리하는 것으로 전시 도메인의 영역에서 사용되는 데이터는 컴퓨팅 리소스를 읽기에만 활용하기 때문에 효율적으로 데이터를 읽을 수 있습니다. 이를 기반으로 하여 자연스럽게 이벤트 기반 아키텍처(EDA)를 구현하려는 팀의 의견이 모아졌고 Kafka를 활용하여 이벤트 기반으로 ReadModel을 구성하는 방향으로 진행하기 시작하였습니다.

이벤트 기반 아키텍처(EDA) 소개

우아한형제들에서도 많은 시스템들이 MSA환경에서 타 도메인간의 시스템에 대한 정보를 이벤트를 기반하여 데이터를 수신하고 있습니다. 이벤트 기반 아키텍처라는 말이 낯선분들을 위해서 잠시 설명해보겠습니다.

이벤트 기반 아키텍처를 설명하기에 앞서 이벤트는 아래와 같은 뜻을 가지고 있습니다.

– 일어난 일의 기록입니다.
– 변경 또는 삭제할 수 없는 변경 불가능한 사실을 캡처합니다.
– 이벤트 소비 시 서비스가 적용하는 로직에 관계없이 발생합니다.
– 무기한 대규모로 유지되며 필요한 만큼 사용할 수 있습니다.
출처: https://cloud.google.com/eventarc/docs/event-driven-architectures

이벤트를 하나의 기록 또는 행위라고 생각해 본다면 한 명의 사용자가 배민스토어에서 [상품을 클릭하고] - [상품을 장바구니에 담고] - [상품을 구매(주문) 하고] - [상품을 결제하고] - [상품을 배송받아 배송 메세지를 확인] 까지 대략적으로 총 5개의 이벤트를 구성할 수도 있습니다. 이외에도 배민스토어를 사용자가 배민스토어를 이용할 때 행동에 따른 무수히 많은 이벤트가 발행되고 있습니다.

이처럼 이벤트 기반 아키텍처는 각각의 서버가 비동기적으로 통신을 하며 이벤트 전달 혹은 수신을 통하여 ‘일어난 일의 기록’을 최대한 활용하여 변경 또는 삭제가 불가능한 기록이 각 도메인별로 관심사에 맞는 데이터를 사용하는 것입니다.

이벤트 기반 아키텍처(EDA) 이점

이벤트 기반 아키텍처를 사용했을 때의 이점은 아래와 같습니다.

– MSA 환경에서 서비스 간에 느슨한 결합을 할 수 있다.
– 발행 또는 구독하는 시점을 지정할 수 있다.
– 비동기 이벤트 기반으로 복원력이 존재한다.

출처: https://cloud.google.com/eventarc/docs/event-driven-architectures?hl=ko#asynchronous_eventing_and_resiliency

가장 큰 이점은 MSA 환경에서 도메인 간에 느슨한 결합을 할 수 있게 되는 것입니다. 이벤트를 발행하는 도메인과 소비하는 도메인이 서로 독립적인 배포를 할 수 있는 시스템으로 설계할 수 있게 됩니다. 또한 다양한 개발 언어와 프레임워크를 통해서도 시스템을 개발할 수 있게 되는 큰 장점이 있습니다.

또한, 각 도메인별로 비동기식으로 시스템 별로 응답을 기다리지 않고 소비하는 도메인에서 이벤트를 수신하여 사용할 수 있을 때 활용되기 때문에 타 시스템의 영향을 크게 끼치지 않습니다.

배민스토어의 이벤트 기반 아키텍처(EDA)

실제 배민스토어에서 어떻게 이벤트 기반 아키텍처를 구성해 사용하고 있는지 소개하겠습니다.

배민스토어는 아래와 같은 다양한 시스템들의 데이터를 전달받으며 배민스토어를 서비스를 제공하고 있습니다.

각각의 MSA 시스템별로 각 도메인별 필수 데이터들을 수신하고 있습니다. (MSA 시스템 -> Domain Event 발행)
배민스토어 전시 시스템에서는 해당 이벤트들을 구독(수신)하고 이를 필요한 데이터별로 ReadModel로 변환 처리를 하고 있습니다.

(예시)

Seller {
  sellerId: "1",
  sellerName: "배민스토어",
  serviceType: "배민스토어",
  logoImageUrl: "https://techblog.woowahan.com/"
  sellerPaymentType: ["카드결제", "복합결제"]
}

data class Seller(
    val sellerId: String,
    val sellerName: String,
    val logoImageUrl: String,
    val serviceType: SellerType,
    ...
)

→ 불필요한 데이터는 수신할 필요가 없다. (sellerPaymentType는 전시에 불필요한 다는 것을 가정)

이처럼 이러한 데이터를 변환을 거친 후에 정말로 필요한 데이터(DTO)를 기반으로 전시에서 필수로 노출되어야하는 데이터로 ReadModel를 구성하고 있습니다. 이렇게 생성된 ReadModel은 DynamoDB 및 Redis에 순차적으로 저장한 뒤 고객에게 서빙하게 됩니다.

2개의 데이터 저장소를 구상한 이유는 각각의 저장소에 다른 장점이 있기 때문입니다. DynamoDB에 일관된 10밀리초 미만 성능, 무제한 처리량의 강점이 존재하며, Redis는 일반적으로 메모리를 이용하여 빠른 데이터를 쓰고, 조회하는 액세스 패턴에서 사용이 가능합니다. 또한, 단일 장애점(SOPF) 문제점을 발생시키지 않기 위해서 Redis에 장애 발생 시 DynamoDB 데이터 기준으로 안전하게 FallBack을 할 수 있게 됩니다. 이러한 시스템을 기반으로 이 두 개의 데이터 저장소를 조합하여 데이터를 사용자에게 전달하고 있습니다.

이러한 장점들을 조합해 안정적이고 빠르게 데이터를 전달할 수 있는 ReadModel을 구성할 수 있습니다.

Kafka – Producer & Consumer

현재 배민스토어에서는 대다수의 데이터를 수신할 때 Kafka Pub/Sub 형태의 구조를 통하여 시스템을 구성하고 있습니다. Seller(본사 정보), Shop(지점 정보)의 정보가 변경되었을 때 Producer를 통해서 이벤트가 Kafka Cluster에 도달하게 되고 본사 정보 및 지점 정보를 구독하는 Consumer가 이벤트를 수신하여 DynamoDB 및 Redis에 데이터를 순차적으로 저장하는 방식을 사용하고 있습니다.

이러한 방식은 이벤트가 발행되었던 시점에 데이터가 캡처되어 해당 데이터를 구독할 수 있는 장점이 있습니다. 즉, 데이터가 변경되는 시점에 대한 이벤트가 캡처되어 이벤트가 재구독이 되더라도 동일한 데이터를 수신할 수 있게 됩니다. 하지만 특정 상황에서 데이터의 정합성을 해칠 수 있는 상황이 발생될 수 있습니다. 이벤트의 순서가 보장되지 않는 상황에서 이벤트의 순서가 역전되는 현상이 발생될 수 있습니다

이벤트 순서

  • [상품 등록 이벤트 → 상품 수정 이벤트] ⇒ 정상
  • [상품 수정 이벤트 → 상품 등록 이벤트] ⇒ 비정상 (대처가 필요함)

잘못된 이벤트 순서가 수신된 경우는 데이터를 보정하는 처리를 수기로 개발하거나, 결과적 일관성(Eventual consistency)을 보장하기 위해서 다양한 검증 처리를 추가해야 하는 상황이 발생될 수 있습니다. 이 문제점을 대처하기 위해서 로그를 남겨두어 지속적으로 모니터링하고 이벤트 순서를 보장하거나 보정할 수 있는 Fallback 로직을 구현하는 방법 등을 구현할 수 있습니다.

배민스토어에서는 지속적인 모니터링을 통하여 이벤트 순서가 역전되는 문제가 발생되었을때 Fallback 로직을 통하여 데이터를 보정하거나, 수기로 보정하는 작업 등을 동시에 진행하고 있습니다.

Zero Payload

Zero Payload는 일반적인으로 사용되는 이벤트 Body에 정보가 담겨있는 형태가 아닌 이벤트 Body에 데이터가 없는 형태로 수신되는 것을 뜻합니다.
(예시 → 상품 정보가 변경되었을 때 변경이 된 Product Id를 수신)

Zero Payload 방식을 사용하면 결과적으로 최종 변경된 이벤트를 기반으로 항상 최신의 데이터를 갱신해 앞서 설명드렸던 이벤트 순서 문제점을 해결할 수 있습니다. 또한, 이벤트에 대한 스키마 스펙이 자주 변경되면 유연하게 스펙 변경에 대처할 수 있는 장점이 있습니다.
(예시 → 상품의 정보는 수시로 스펙의 변화가 잦게 발생될 수 있음)

예를 들면 상품의 정보 처리 중에 가격의 정보가 잘못된 이벤트 순서로 수신되면 사용자는 잘못된 데이터를 상품 구매 목록에 전달되는 불상사가 생길 수 있습니다.

배민스토어의 상품 정보에 구매 시 고려할 중요한 정보(가격, 재고, ETC)들은 Zero Payload 방식을 사용하고 있습니다. 데이터 저장 시 실시간으로 HTTP API 호출을 통하여 변경된 이벤트 중 가장 마지막 정보를 DynamoDB, Redis에 저장하여 사용자분들에게 올바르게 데이터를 노출하게 됩니다.

이벤트 기반 아키텍처(EDA) 명과 암

이벤트 기반 아키텍처는 배민스토어 전시 도메인에서는 적극적으로 활용하고 있습니다. 다양한 도메인을 연결할 때 느슨한 결합에서 가장 큰 이점을 활용할 수 있고 뛰어난 복원력을 통해서 기존에 이벤트를 재연결 하는 등 작업을 진행하기도 합니다.

이러한 큰 장점에서도 다소 아쉬운 점들은 존재합니다. 이벤트 기반 아키텍처는 다른 아키텍처에 비해서 구성하기 어려운 점이 있고, 디버깅이 어려운 점도 존재합니다.

출처 – 대학일기 웹툰

하지만 이러한 문제점들을 해결하기 위해서 소비가 된 이벤트를 재발행하여 처리하거나, 집중적으로 모니터링을 통해서 해결하는 방법 등을 활용할 수 있고 더 나아가 DeadLetter 토픽을 활용하여 실패된 이벤트를 재사용을 처리하는 로직을 구현할 예정입니다.

많은 도메인을 이벤트 기반하여 비동기 통신으로 시스템 아키텍처를 구현하였지만 HTTP API를 통하여 연동되는 시스템도 존재하고 있습니다. 이러한 시스템은 연관되어 있는 시스템의 상황에 맞추어 배민스토어의 회복력(Resilience)을 고려하였으며 resilience4j를 활용해 Circuit Breaker를 적용하여 안정적인 시스템을 만들기 위해서 노력하고 있습니다.

참고: Circuit Breaker

마무리

배민스토어 전시개발팀(현 서비스개발팀)에 처음으로 합류했을 당시에서는 도전이 필요했던 상황입니다.

미션 리스트

  • 신규 전시 시스템 아키텍처 개편
  • 성능 향상
  • 안정적인 시스템 구현
  • 이외

실제로 이벤트 기반 아키텍처로 변경하려고 했을 때 올바른 아키텍처인지 실제로 많은 고민을 하고 팀 내에서 많은 논의를 진행하여 과연 정상적으로 아키텍처를 변경할 수 있는지 의구심이 들기도 하였습니다. 시스템 개편을 진행하는 당시에 실제로 많은 시행착오를 겪기도 하였으며, 오픈을 며칠 안 남은 상태에서 성능 테스트에서 기대에 못 미치는 성능이 노출되는 등 어려움이 많았습니다.

[이벤트 기반 아키텍처 변경 + 데이터 저장소 변경 + Spring Webflux] 시스템을 개편을 통하여 서비스의 구조를 완료 한 후 배민스토어의 현 전시 도메인 시스템 성능은 아래와 같이 상승했습니다.

배민스토어 사용자께 안정적인 시스템을 제공하기 위해 시스템은 꾸준히 진화하고 있습니다. 앞으로도 더 많은 변화가 있을 예정이며 많은 관심 부탁드리겠습니다. 그리고 앞으로 우아한 형제들에서 함께 시스템을 만들어 나아갈 분들은 언제든 환영입니다!


[배민스토어] 일반셀러 프로젝트 시리즈 더 보기