Skip to content

progress0407/e-commerce-with-msa

Repository files navigation

e-commerce with MSA

MSA에 대한 Best Practice 보다는 탐구 목적에 가깝게 개발하였습니다
성공적인, 훌륭한 프로젝트는 아님을 말씀드립니다. 해당 프로젝트를 Good Practice로 삼으면 안됩니다
Chris Richardson의 Microservices Patterns에서 나오는 가상의 Food 서비스의 내용을 생각하며 되짚어본 프로젝트입니다

프로젝트 설명

모듈 명 한글 뜻 쉬운 설명 (?)
eureka 서비스
디스커버리
마이크로 서비스를 이름을 통해 실제 IP, Port를 찾을 수 있게 한다 (DNS의 개념)
마이크로서비스 간의 약한 결합을 가능하게 해준다
api-gateway 게이트웨이 각 마이크로 서비스를 라우팅(길찾기)하고
인가/로깅 등의 부가 기능(edge)을 수행한다 (reverse-proxy)
user 사용자·인증 사용자 정보를 관리하고 인증을 담당(접근 권한 발급)하는 서비스
item 상품 상품을 관리하는 서비스
Command(등록, 수정, 삭제)의 경우 상품 관리자가 이용한다고 가정했다
coupon 쿠폰 상품 할인 쿠폰 관리하는 서비스
고정 할인 / 비율 할인이 있다
order 주문 상품들에 대한 주문을 관리하는 서비스
payment 결제 (검토 예정 -> 현재 보류) PG사를 통해 결제를 하는 서비스
(실제로는 PG 연동을 하지 않고 모의 상황을 가정했다)
query 조회 (구현 예정) BFF, 클라이언트를 위한 조회 서비스
여러 마이크로서비스의 Datasource가 필요한 Read(통계 등)는 이곳에서 담당한다
이벤트 소싱 방식 채택

하위 도메인 관계도 (Context Map)

image
  • 본디 이루어졌어야 할 논리적인 도표이며 실제 프로젝트 내부는 메세지큐에 상호 의존성이 존재
    • 자세한 내용은 아래에 기술
  • user는 모든 도메인의 Upstream이다

요청 흐름

주요 UseCase

사용자의 주문 처리가 핵심 비즈니스이다

  • 사용자는 주문 화면에서 다음과 같은 정보를 확인할 수 있다
    • 주문할 상품의 정보 (이름, 가격, 수량)
    • 사용할 수 있는 쿠폰의 존재 여부
  • 적용할 수 있는 쿠폰이 있다면 각 상품에 쿠폰을 적용한다
    • 이때 하나의 상품에 2개 이하의 쿠폰 적용 가능
    • 적용된 상품은 일정 금액 이하로 내려갈 수 없다
  • 주문을 한다
    • 상품의 원가와 할인가 모두 서버에 전송한다

주문 요청이 성공한 경우

도표 image

요청을 받은 주문 서버에서는 주문을 바로 처리하지 않고 대기 상태로 둔다(PENDING)
그리고 각 마이크로서비스에게 비동기 요청을 한다

상품 서비스는 아래의 사항을 검증한다

  • 상품의 재고가 현재 존재하는지
    • 주문서를 작성하는 도중에 다른 사용자로 인해 재고가 마감될 수 있다 (무신사 블랙프라이데이를 생각하자...)
  • 현재 가격 정보와 요청 정보가 일치하는 지

검증에 성공하면 상품 재고 수량을 차감하고 정상 응답을 한다

쿠폰 서비스는 아래의 사항을 검증한다

  • 쿠폰이 존재하는지
  • 쿠폰을 비즈니스 정책에 맞게 사용한 것인지
  • 실제 쿠폰 할인가와 요청한 쿠폰 할인가가 일치하는지
    • 클라이언트 요청은 항상 위변조가 가능하다!

검증에 성공하면 쿠폰 사용여부를 true로 하고 정상 응답을 한다

그리고 주문 서버로 응답을 한다
주문 서버는 (스케줄링) 응답을 리스닝하고 있다가 모든 응답이 정상임을 확인하고 주문 완료 처리를 한다


주문 요청이 실패한 경우

도표 image

주문 요청을 한 이후 상품, 쿠폰 서비스 둘 중 하나 이상이 검증에 실패한 상황이다
이 경우 검증에 성공하여 데이터를 변경한 서버들에게만
현재 요청이 실패했음을 알려서 데이터를 이전 상태로 복구시켜야 한다 (이것을 보상 트랜잭션이라 한다)

위 도표의 예시는 쿠폰 서비스에서는 검증이 실패하여 데이터 변경을 하지 않고,
상품 서비스는 검증에 성공하여 데이터 변경이 일어난 경우이다
응답 받은 이벤트 중 검증에 실패한 이벤트가 있음을 확인한 주문서비스는 PENDING상태에서 FAIL로 변경한다
그리고 검증에 성공한 상품 서비스에게만 보상 이벤트를 보낸다
이벤트를 수신한 상품 서비스는 데이터를 롤백하고 주문 서비스에게 응답한다
롤백 응답을 받은 주문서비스는 해당 주문건을 CANCEL상태로 만든다

설계상 고민한 점

분해 전략

  • DDD의 sub-domainbounded-context를 통해 각 마이크로 서비스를 분리하고자 함
    • 각 비즈니스 영역이 다루는 문제가 존재
      예를 들어 옷을 주문하는 사람과 등록하는 사람, 쿠폰을 제공하는 운영진은 웹사이트를 다루는/바라보는 관점이 모두 다르다
      따라서 각 하위 도메인마다 요구하는 비즈니스의 성질이 다를 것이므로 상품, 주문, 쿠폰의 하위 도메인으로 분리

데이터 정합성 (Atomic)

  • 분산 레벨에서의 트랜잭션
    • Saga 편성 중 코레오그래피 방식을 이용하여 예외 상황에 대한 rollback
      • 오케스트레이션 편성의 경우 Axon등 라이브러리 의존성이 생겨서
        MSA 자체 문제의 집중보다는 라이브러리를 익히는 것에 집중이 분산될 것으로 보여서 선택을 보류했다
    • Outbox를 통해 브로커가 장애가 났을 경우에 대해 보완 가능
      • 브로커 복구시 Scheduler가 OutBox테이블에서 미발송된 이벤트를 재 적재한다
      • 현재 멀티 인스턴스에서의 스케줄링은 고려되지 않았다 (동시 접근 문제 -> 중복 데이터 발행)
        • 해결 방안: shed 락, 스케줄러 서버 분리 (오직 하나만 존재)
  • 로컬 레벨에서의 트랜잭션
    • 조회시 버전 증가 잠금을 통해 애그리거트의 정합성이 깨질 위험을 방지 (낙관적 락)
      • 애그리거트 루트뿐만 아니라 구성요소가 바뀌는 경우를 고려해야 하기 때문에 조회를 할 때부터 읽어야 한다
    • 이 부분이 이루어지면 분산 시스템에서의 동시성 문제가 해소된다
      어느 한 마이크로서비스에서 올바르게 검증이 실패하면 전체 트랜잭션이 실패하여, 전체 시스템의 데이터 정합성을 지킬 수 있다

성능

  • 비동기 호출: 주문(POST /orders)의 경우 되도록 동기 통신이 아닌 비동기 통신을 구현하고자 노력
    • 이 부분이 잘 이루어질 경우 DB Connection 선점 시간을 짧게 가져갈 수 있다
  • Table 복제: 타 마이크로 서비스의 테이블의 일부 데이터를 복제하여 통신을 일부 제거하고자 노력
    • 다만, 각 마이크로 서비스 간의 결합도가 생긴다 (분산 모놀리스가 될 위험성 존재)

유지보수 성

  • spring-cloud-gateway의 route 코드: API가 추가될 수록 관리가 용이하기 힘들기 때문에 kotlin의 문법을 통해 간소화하고자 함
    • kotlin-dsl로 구현한 테스트 코드를 통해 라우트 정보가 올바로 등록했는 지 알 수 있다
  • 공통 모듈
    • common 모듈을 성격에 따라 구분해서, 각 모듈이 필요한 모듈을 import할 수 있다
  • 코드 레벨
    • human-readable한 코드를 작성하려고 노력했다 (네이밍 컨벤션, 약어 제한 등)
    • Restful API 원칙 중 Uniform interface를 지키고자 노력 (자원, 행위)
    • 통합 테스트를 작성하려 노력 (진행 중/전면 재작성 필요 - contract test)
      • 기본 베이스는 인수테스트로 하되, 이 중 then절만 DB 조회를 통해서 테스트

조회 모델 분리 (구현 예정)

  • 조회 담당 서버를 분리하여 각 마이크로서비스에서 발생한 변경분을 replay하여 데이터를 구성 (이벤트 소싱)

사용자 use-case를 다소 고려하여 API를 설계

  • 무신사, 쿠팡 등을 참고하여, 실제 사용자의 API 호출을 가정하여 작성

이 프로젝트의 한계

실제로 MSA의 극한까지 Best Practice를 지키면서 만들지는 못했다

  • 각 마이크로 서비스가 충분히 크지 않다
    • 현업 개발·기획자들이 공감할 수 있을 정도로 충분히 크지 않다
    • 실제 이용자 수가 많은 서비스와 비교하면 디테일함이 많이 부족하다
  • 각 JVM 인스턴스가 차지하는 성능
    • 로컬에서 6개 이상의 서버가 구동될 때, 성능 부담이 된다 (많게는 8.9Gi ~ 10.gGi 메모리 사용)
    • 로컬 환경 스펙 (노트북)
      • CPU: 3.2Ghz 8Core / Memory: 28GB / SSD
  • 현재까지 이루지 못한 약한 결합
    • 마이크로서비스의 장점과 메세지 큐의 존재 의의 중 하나는 약한 결합(low coupling)이다
      하지만 현재 시점(24.2)에서는 실패했다. 메세지 큐의 이름과 이벤트의 데이터는 논리적으로 상호 참조를 하고 있다
      이상적인 구조라면 Upstream(Publisher)은 Downstream(Subscriber)를 물리적인 것 뿐만 아니라 논리적으로도 참조를 하면 안 된다

하지 못한 것

  • 실제 결제 연동
    • 토스 PG 등 실제 결제 연동까지 진행하지는 못했다
    • 또 편의상의 이유로 실제 결제의 흐름을 축약/왜곡하여 작성하였다
  • 브로커 클러스터에 문제가 생긴 경우에 대한 장애 처리
    • 이 부분까지는 진행하지 못했다
  • 물리적으로 분리되지 않은 DB
    • 하나의 물리 DB를 논리적으로 나누어 사용하고 있음
    • 로컬 성능/비용의 부담으로 실제와 가깝게 구현하지 못함

현재 상황

여러 일정으로 잠정적으로 보류되어왔던 프로젝트입니다...
여유가 될 때마다 구현하고 있습니다 :)

작업 체크 리스트

자세히
  • (마이그레이션) 기존 DDD 프로젝트 (import by Git Subtree)
  • Java -> Kotlin 으로 언어 변경
  • Library 버전 최신화 (to Spring Boot 3.x)
  • 멀티 모듈 프로젝트로 전환 (itme, order)
  • Eureka Module 개발
  • API Gateway Module 개발
  • 공통 Module 추출 (common)
  • RabbitMQ 연동 후 주문 상품 이벤트 pub-sub 개발
  • H2 -> MySQL로 DB 변경
  • RabbitMQ -> Kafka로 전환
  • ( 마이그레이션) User Module 가져오기
  • 쿠폰 Module 개발
  • Neflix Passport 구현
    • 보류 (동기 호출시 Blocking 예외 발생)
    • 대안으로 토큰 검증 필터 구현
  • Order -> Item, Coupon: 주문 생성 이벤트 검증부 구현
  • 마이크로 서비스 2-depth 멀티 모듈로 그룹화
  • p6spy 로그 포맷터 적용
  • Coupon 가격 계산 API 구현
  • repository.saveAll()에서 data.sql 로 초기화하는 구조로 변경
  • Item -> Coupon, 상품 Semi 데이터 이벤트 발송 기능 구현
  • 마이크로 서비스 내 애그리거트 트랜잭션 충돌 방지
    • 애그리거트 내 구성요소가 바뀔 경우 고려
    • 애그리거트 수정시 조회 메서드에 LockModeType.OPTIMISTIC_FORCE_INCREMENT 적용
  • Order <-> Coupon, Item 보상 트랜잭션 발신, 송신 구현
  • Canceled 상태 추가 및 로직 구현
  • Order 바운디드 컨텍스트에 Orderer 추가
  • k8s 배포 스크립트 작성
  • outbox 동시성 문제 해결

About

MSA를 탐구하기 위해 시작한 토이 프로젝트

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages