메뉴

문서정보

목차

음식 배달 앱 아키텍처 분석

이커머스 아키텍처링 문서들을 찾다가 End to End Design of food delivery app 문서를 발견했다. 이커머스와 정확하게 일치하지는 않지만 매우 비슷한 서비스이고 사용하는 기술도 비슷해서 분석하면 좋겠다 싶어서 정리하기로 했다.

위의 문서의 많은 부분을 참고하겠으나 번역 문서가 아니다.

아래의 방식으로 정리할 생각이다.
  1. 음식배달의 사용자 라이프 사이클
  2. 요구사항 정의
  3. 컴포넌트 다이어그램
  4. 핵심 서비스들에 대한 상세 시퀀스 다이어 그램
  5. 각 서비스들의 인프라 아키텍처
  6. 도전과제 및 핵심 기술 들에 대한 솔류션 제안
  7. AWS, EKS 기반의 전체 애플리케이션 아키텍처
 Food delivry App architecture

Zomato, Swiggy 에 대한 대략적인 분석

나름대로 아키텍처를 하겠는데, 위에 있는 Zomato, Swiggy의 설계 문서를 대략 분석해보려 한다. 많은 고민이 들어가 있는 아키텍처일테니, 분석하는 것만으로 얻는 정보들이 있을 것이고 이를 기반으로 생각을 정리하는게 좋은 방향이라고 생각해서다.

Lifecycle of a food delivery

음식 배달앱의 사용 시나리오를 다이어그램 형태로 묘사한다. 다양한 예외 경우를 제외한 서비스의 핵심 흐름을 묘사하면 되는데, 이를 이용해서 필수 기능들을 식별 할 수 있다.

 Lifecycle of a food delivery

  1. 앱을 실행하고 초기화 한다. 음식 배달서비스는 위치를 기반으로 하기 때문에 이때 위치 정보를 수집 할 것이다.
  2. 수집된 위치정보를 이용해서 근처 음식점과 음식 메뉴를 검색한다.
  3. 메뉴를 선택하고 Cart에 담는다.
  4. 주문 한다. 배달 거리, 할인, 마케팅, 프로모션, 할인율 등에 따라서 지불금액이 결정된다.
  5. 지불한다. 배달 완료 후 지불하는 방법도 있는데, 시나리오를 단순화하기 위해서 즉시 주문만 가능하도록 했다. PG 서비스를 이용해서 지불 할 것이다. 지불이 완료되면 음식점에 알림이 전달되고 드라이버가 할당된다.
  6. 사용자 앱에 ETA(예상 도착시간)이 표시된다. 이 예상도착시간은 음식점 앱에도 표시된다.
  7. 배달이 완료된다. 드라이버 앱과 배달 앱 상호간에 커뮤니케이션이 발생한다. 음식점 앱에 완료 메시지가 전송된다.
  8. 영수증이 출력된다. 영수증을 출력하기 위한 정보는 서버에 저장된다. 배달 거리, 드라이버, 목적지, 메뉴, 가격, 실행된 할인/마케팅/프로모션, 배달가격
  9. 사용자는 음식점 서비스에 대해서 피드백한다.

기능 요구사항

Lifecycle of a food delivery롤 기반으로 음식 배달 시나리오에서 아래의 기능 요구사항을 도출 할 수 있다.
  1. 음식점 온보딩
  2. 음식점과 메뉴의 검색 : 음식점과 메뉴를 업로드 하고 이를 승인하기 위한 관리자 애플리케이션이 필요하다.
  3. 동적인 가격 적용 : 프로모션, 할인, 배달 비용. 배달 비용은 거리에 따라서 달라질 수 있다.
  4. 주문관리 : Cart 및 결제, 주문의 상태변화(주문 시작, 조리 중, 배달 중, 완료)
  5. PG(Payment Gateway) 서비스를 이용해서 음식값을 지불 한다.
  6. Door to Door Delivery : 드라이버 매칭, 위치 업데이트, ETA
  7. 사용자 행동(Activity), 사용자 위치, 프로모션 등을 기반으로 하는 식당 & 메뉴 추천 및 검색 시스템
  8. 알림 서비스
  9. 피드백 & 리뷰 시스템
  10. 판매 보고서
  11. 빌링

비기능 요구사항

  1. 하루 백만개의 주문을 처리 할 수 있어야 한다.
  2. 하루 천만개의 검색을 처리 할 수 있어야 한다.
  3. 모든 거래 데이터가 기록되어야 하며 손실이 있어서는 안된다.

핵심 컴포넌트 식별

이 서비스의 핵심 컴포넌트는 Order ServiceDelivery Service로 정의 할 수 있을 것이다. 기타 서비스들을 살펴보면 들이 있다. 도전적인 기능은 아니라고 생각해서 이들은 자세히 다루지 않을 것이다.

API Gateway 패턴

API Gateway 패턴을 사용하고 있다. API Gateway는 특히 MSA에서 널리 사용하고 있는 패턴이다. 요즘 클라우드 네이티브에서 뭔가 서비스하겠다고 하면 API Gateway는 거의 필수로 사용한다고 보면 되겠다. API Gateway 패턴은 클라이언트와 서비스(백앤드)를 분리하며, 클라이언트의 요청과 서비스 응답을 통합관리하는 독립계층으로 작동한다. 권한/인증, 라우팅 관리, 요청/응답 검사, 프로토콜 관리/변환, 보안과 같은 처리를 API Gateway에 위임 할 수 있다.

아래와 같이 간략하게 묘사할 수 있을 것이다.

 API Gateway 패턴

크게 Admin, Restaurant, User(Customer)의 API Gateway로 구성이 된다. 기능별로 서비스를 분류해서 API를 구성을 했다. 여기에서 이슈는 특정 분류의 서비스가 다른 특정 분류의 서비스를 호출하는 경우가 있다. 예를 들어 Admin 서비스는 레스토랑 관리자를 위한 서비스인데, 레스토랑 사용 유저를 관리 해야 할 수 있으므로 Restaurnat API Gateway에 있는 User 서비스를 호출해야 할 수 있다.

두 가지 방법을 생각해 볼 수 있다.
  1. Admin App이 유저 정보가 필요 할 때는 Restaurants API Gateway를 호출한다.
  2. Admin App이 유저 정보가 필요 할 때는 Admin API Gateway에서 User Service를 호출한다.
두 번째 방법이 더 간단할 것이다. 첫번째 방법을 사용 할 경우 Restaurants API Gateway는 Admin 유저도 관리를 해야 하는데, 일반 사용자를 관리하는 Restaurants API Gateway 목적에 맞는 데이터가 아니다. Admin 권한은 Admin API Gateway 에서 일원화해서 관리하는게 좋다.

두 번째 방법은 서비스 내부에서 API를 호출하는 방식이 될 것이다. User 서비스는 외부에의 호출과 내부에서의 호출을 분리할 필요가 있다. Admin의 경우 모든 유저를 전부 관리해야 겟으나, 외부에서 호출하는 경우에는 권한이 제한 될 것이기 때문이다. User 서비스에서 호출의 권한을 확인하도록 미들웨어를 구성하는 것이 쉬운 방법이다. 권한 확인을 위한 기능은 User 서비스 뿐만 아니라 다른 모든 서비스에도 미들웨어 형태로 작동하도록 한다.

 Middleware Get User Info

  1. Admin API Gateway로 요청이 들어온다.
  2. Admin API Gatway는 Admin 권한을 가진 요청인지 확인한다.
  3. 요청을 User 서비스로 전달한다. 이때 인증 토큰을 함께 전송한다.
  4. User 서비스 미들웨어는 인증토큰을 읽어서 권한을 확인한다.
  5. 권한에 따라서 쿼리를 실행하거나 거부한다.
User 서비스는 하나의 API에서 분기 할 수 있겠지만 /admin API를 따로 만들어서 명확히 분리하는 것도 고민해 볼 수 있다. 보통은 기능으로 분리하지만 Admin 과 같은 특수한 권한의 경우에는 분리하는게 깔끔할 수 있다.

주문(Order) 서비스

주문 서비스는 기술적으로 다른 서비스와 차이가 있다. 프로세스는 아래와 같을 것이다. 주문전 지급(Payment)하는 방식이다.

  1. 사용자는 App을 이용해서 음식을 선택하고 주문을 한다.
  2. Payment Service를 이용해서 결제한다.
  3. 음식점으로 주문이 전달된다.
  4. 음식점은 주문을 검토하고 수락한다.
  5. 배달을 할 수 있는 조건의 라이어들에게 메시지를 push 하고, 수락한 드라이버에게 작업이 할당된다.
  6. 음식이 만들어지고 드라이버는 배달한다.
  7. 소비자에게 음식이 전달되고, 작업이 완료 된다.
이 과정은 "Transaction(거래)"의 상태변화로 관리 할 수 있다. 배달 서비스에서 트랜잭션의 상태변화는 아래와 같이 정의 할 수 있을 것이다.

 Transaction ...

Transaction은 주문의 생성에서 완료까지의 모든 상태정보를 데이터베이스에 저장한다. 키는 Transaction ID 혹은 Order ID가 될 것이다. Transaction 데이터베이스에는 Insert 만 한다.

 Order 시스템

모든 주문요청은 OrderDB에 저장된다. 지불이 완료되면, 음식점으로 요청이 전달된다. 음식점이 주문을 수락하면, 드라이버에게 주문이 Publish 된다.

Order Service에서의 CQRS 패턴

Order Service는 CQRS 패턴을 사용한다. 즉 쓰기와 읽기를 분리 한다. CQRS 패턴을 사용하기 전에 사용하는 이유를 살펴보자.

기존의 아키텍처에서 데이터베이스의 쿼리와 업데이트에 동일한 데이터 모델을 사용했다. 이 모델은 간단하고 기본적인 CRUD 작업에 적합하다. 하지만 복잡한 애플리케이션에서는 이 방법을 사용하기 어렵다. 쿼리가 복잡한 애플리케이션을 개발 할 때, 쓰기와 읽기의 데이터 모델이 서로 달라져야 하는 걸 경험해 본적이 있을 것이다. 이러면 모양이 다른 DTO(데이터 전송 개체)를 반환하게 되는데, 개체 매핑이 복잡해 지면서 애플리케이션의 복잡도를 높이게 된다.

읽기와 쓰기는 성능에 대한 요구사항도 다르기 때문에 이들을 단일 데이터베이스 모델로 묶는 것은 좋은 방법이 아니다.

CQRS는 데이터의 쓰기(혹은 업데이트)와 읽기 쿼리를 이용해서 읽기와 쓰기를 다른 모델로 분리 한다. 아래는 CQRS를 묘사하고 있다.

 CQRS 묘사

Order Service에 CQRS 패턴을 구성해보자.

 주문에서의 CQRS 패턴

주문을 크게 두 개의 영역으로 분리했다.

Order Service : 주문의 상태를 관리하는 서비스로 주문의 Create 에서 완료 까지의 전체 상태를 관리한다. 주문의 모든 상태 변화는 트랜잭션 테이블에 저장한다. 트랜잭션 로그이기 때문에 인서트만 있다.

주문 수행 서비스 : 주문을 수행하기 위한 Restaurant Service와 Delivery Service로 구성된다. 이들은 작업을 수행하고 수행 결과를 Order Service에 보고 한다.

Order Service로 모니터링된 모든 정보는 Order Index Table에 업데이트 된다. 사용자들은 이 서비스를 이용해서 주문의 현재 상태를 확인 할 수 있다. Order Service는 모든 작업에 대한 Transaction Log 로그를 남기는데, 이 데이터를 이용해서 주문의 상세정보를 제공 한다. 이 정보는 사용자와 관리자 모두가 사용할 수 있다.

Order Service는 Restaurant, Delivery 서버스를 호출해서 주문을 처리하는 작업을 조율하는데, 작업 요청은 Order Event Queue로 전송된다. Restaurant, Delivery는 Queue에서 작업을 읽어서 처리하고 그 결과는 다시 Order Service에 보고한다. Order Service는 보고를 처리하고 Transaction Log에 저장한다.

주문과 관련된 모든 이벤트는 Order Activity Event Capture에 의해서 Order Index Table에 색인된다. 이 테이블을 조회하여 주문에 대한 상태를 확인 할 수 있다. Order Index Table은 Order Transaction Table의 View Table 처럼 작동 할 것이다.

실제 구현에서는 세부적으로 다르게 구성 할 수 있을 것이다. 예를 들어 Restaurant & Delivery 서비스도 Order Service의 API를 통해서 보고하는 대신, Event Queue에 메시지를 publishing 할 수 있을 것이다.

Delivery Service

음식점에서 주문을 수락하면, Order Service는 Order 큐에 메시지를 전송한다. 이제 Delivery Service가 호출되고 Driver Finder가 큐를 읽어서 배달작업을 수행할 드라이버를 찾는다.

Driver Find는 위치기반 기반 서비스다. 당연히 음식점과 가까운 거리에 있는 드라이버를 우선해서 찾아야 하기 때문이다. 따라서 드라이버는 모바일 기기에 설치된 드라이버 앱을 통해서 주기적으로 자신의 위치를 알려줘야 한다.

여기에서 우리는 Driver Find 서비스는 아래의 구성요소로 이루어짐을 알 수 있다.
  1. 드라이버 위치 색인 서비스
  2. 위치 색인 서비스로 부터 드라이버를 검색하는 서비스
시스템을 구성해보자.

 Driver Finder Service

드라이버 앱은 드라이버의 위치와 상태를 계속 전송한다. 이 정보는 kafka로 전송되며, Driver (위치)indexer가 색인한다. 위치 색인은 H3, GeoHash 등을 사용한다.

음식점이 주문을 수락하면 Delivery Service를 호출한다. Deliver Service는 Driver Find Service를 호출해서, 반경내 드라이버를 찾아서, 메시지를 Push 한다.

Payment 서비스

대한민국에서 Payment 서비스는 PG(Payment Gateway, 전자지급결제대행)를 이용한다. PG는 온라인 대표 가맹점의 역할을 수행하는 동시에 온라인 가맹점에게 지급결제 서비스를 제공한다. PG 사의 구조는 아래와 같다.

 PG & VAN

VAN사는 VAN(Value Added Network, 부가가치 통신망)을 구축하고, 제 3자에게 이 서비스를 판매하는 네트워크 사업자를 의미한다. 이 네트워크는 지급/결제 서비스를 제공한다. VAN사의 비즈니스 모델은 것이다. 오프라인 마케팅 능력이 중요한 영역이라고 할 수 있다.

온라인은 오프라인과 상황이 많이 다르다. 온라인은 영업을 할 오프라인 매장이 있는 것도 아니며, 오프라인과 달리 매우 빠르게 개점과 폐업이 발생한다. 오프라인에서 하던 것처럼 찾아다니면서 영업을 할 수 없는 것이다.

이때 PG사가 역할을 한다. PG 사는 기본적으로 VAN 사의 가맹점인데 일반 가맹점과는 좀 특수한 목적을 가진다. 즉 온라인 가맹점을 대신해서 모집하는 대표 가맹점의 역할을 한다. 전자상거래 기업은 PG 사에서 제공하는 결제 소프트웨어를 이용해서 고객에게 결제 서비스를 제공한다.

Kafka를 이용한 Eventual consistency

Order Service는 상품과 돈의 거래에 관련된 중요한 정보를 처리하는 시스템이다. 시스템은 거래의 일관성을 보장해야 하는데, Kafka를 이용해서 최종 일관성(eventual consistency)를 달성했다.

우리는 왜 최종 일관성모델을 사용했는가 ? 그것은 MSA 모델을 사용했기 때문이다.

MSA 모델은 분산 시스템인데, 분산 시스템은 강력한 일관성을 달성하는데 많은 비용이 들어간다. 이는 주로 2단계 커밋이라는 분산 트랜잭션의 요구사항 때문이다. 그리하여 마이크로 서비스 아키텍처는 강력한 일관성 보다는 최종 일관성을 선택하게 된다.

트랜잭션은 일반적으로 ACID라고 약칭되는 원자성(Atomicity), 일관성(Consistency), 격리(Isolation), 내구성(Durability)의 주요 4가지 속성을 가지고 있다. 마이크로 서비스의 주요 목표는 느슨하게 결합된 아키텍처를 유지하면서 저렴한 비용으로 ACID를 달성하는데 있다. 최종 일관성이라고 하면, ACID와 유사한 결과를 달성하는 것이라고 볼 수 있다.

최종 일관성 달성의 어려움

강력한 일관성은 서비스에 있는 개체의 상태변화가 해당 개체를 관찰하는 다른 서비스에서도 즉시 반영된다는 것을 의미한다. 강력한 일관성은 일반적인 동기 함수 호출 또는 동기 메시징을 통해서 달성할 수 있다.

아래 그림은 강력한 일관성을 제공하는 주문 처리 시스템을 묘사하고 있다.

 강력한 일관성

이 시스템은 서비스간에 강력한 일관성을 제공한다. Update와 Insert가 성공적으로 호출된 후에만 작업이 완료되기 때문에 데이터베이스에 의존하게 된다.

이 예에서 두 번째 Update 요청의 처리는 첫 번째 Insert(주문생성)의 성공적인 처리에 전적으로 의존하기 때문에 동기적인 메시징이 필요하다. 이런 동기식 호출에는 백앤드 주문 서비스와 웹 애플리케이션간이 높은 결합도를 가지게 된다.

마이크로 서비스에서 서비스는 자율적으로 분리되기 때문에, 서비스들이 이렇게 긴밀하게 연결되는 것을 좋아하지 않는다. 서비스들의 결합도가 높아지게 되면, 다른 서비스에 종속이되며 이는 시스템의 확장을 제한한다. 이러한 문제는 백앤드 주문서비스가 주문 상태 변경을 비동기적으로 브로드 캐스트하고, 이 상태를 구독하는 애플리케이션이 데이터를 올바르게 처리하고 동기화하도록 하는 비동기 메시징을 해결 할 수 있다.

비동기 메시징의 문제와 처리

비동기 메시징의 일반적인 문제는 구독자가 이벤트를 논리적 순서로 수신할 수 있다는 보장이 없다는 것이다. 이 예에서 웹 애플리케이션은 주문 생성 메시지를 수신하기 전에 주문 확인 메시지를 수신 할 수 있다.

 비동기 메시징의 문제

위 시나리오에서는 Insert가 완료되지 않았기 때문에 Update가 실패한다. 이 경우 웹 애플리케이션에 주문 확인 실패 메시지가 도착할 것이다. 웹 애플리케이션은 백앤드 주문 서비스와 분리되어 있기 때문에, 오류의 원인을 감지하지 못하고 주문 확인 요청 메시지를 다시 보낼 것이다.

가장 빠른 해결 책은 "주문 확인 메시지를 웹 애플리케이션에 보관하는 방식일 것이다." 하지만 이 방식 보다는 Kafaka Streams를 이용하는 것이 더 좋겠다. 카프카 스트림은 아래와 같은 특징을 제공한다.

카프카 스트림은 이 문서의 범위를 벗어난다. 다른 문서를 통해서 자세히 다루도록 한다. 여기에서는 카프카 스트림을 이용해서 어떻게 최종 일관성을 달성 할 수 있는지를 간단히 설명한다.

  1. 주제(Topic)에 메시지가 생성되면 Kafka는 컨슈머가 메시지를 한번 이상 수신 하도록 보장한다. 이를 통해서 컨슈머의 실패, 메시지 재전송 등에 대해 걱정하지 않고 메시지를 사용하는 서비스들을 확실하게 분리 할 수 있다.
  2. 서비스는 병렬로 분할된 주제에 대해서 안전하게 처리 할 수 있다.
  3. 상태 저장소를 사용하면 기본 키로 메시지 상태를 유지하고 추적할 수 있다. 이 상태 데이터를 이용해서 새 메시지를 처리하거나 이전에 실패한 메시지를 다시 처리하는데 사용 할 수 있다. 다른 솔류션과 달리 상태 저장소에 대한 접근은 로컬이다. 따라서 네트워크 대기시간이나, 외부 데이터베이스나 캐시에 대한 접근이 필요하지 않다.
  4. 기본키를 이용해서 이벤트를 시간순으로 순차적으로 처리할 수 있는 직렬화된 격리 수준(serialized isolation level)을 제공한다. 직렬화된 격리 수준에 대해서는 데이터베이스 Transaction Isolation Level 문서를 참고하자.
아래 그림은 카프카 스트림으로 최종 일관성의 구현을 묘사하고 있다.

 카프카 스트림을 이용한 최종일관성

Kafaka를 이용한 최종 일관성은 별도의 문서로 다룬다.

설계 및 기술셋 결정사항

이상에서 우리는 아래와 같은 설계/기술셋을 설정했다.

참고