주문 결제 트랜잭션 단위를 어떻게 가져가야 할까
이번에 이커머스 서비스를 만들면서 주문 결제 트랜잭션 단위를 어떻게 설계할 것인지에 대해 고민을 많이 하게 되었습니다.
우선 이 서비스에서 결제는 별도의 pg사를 연동하지 않고 user 테이블에서 point를 관리하여 이 point로 결제를 하는 서비스입니다.
주문 결제 트랜잭션을 분리하는 방안과 하나의 트랜잭션으로 가져가는 2가지 방안에 대해 어떤 방법이 더 적절(?)한 지 고민하고 생각했던 내용을 정리해보려고 합니다.
사실 "이 방법이 무조건 맞아!" 같은 정답이 없다는 건 머리로는 알지만 이런 선택을 해야 할 때는 누군가 정답을 줬으면 좋겠다는 마음이 있습니다. 아직까지는 트레이드 오프하는 게 쉽지 않은 것 같습니다..
주문 결제 트랜잭션 분리
우선 주문과 결제를 2개의 트랜잭션 단위로 나누는 방법을 선택했습니다.
하나의 트랜잭션 단위로 가져가게 되면 사용자가 주문 -> 결제까지 락을 잡고 있는 시간이 길어져 다른 사용자의 대기 시간이 길어져 사용성 저하 문제가 있을 것이라고 생각했습니다.
그래서 트랜잭션의 범위를 짧게 가져가서 락을 잡고 있는 시간을 줄여 사용성과 성능을 증가시키기 위해 주문 트랜잭션과 결제 트랜잭션으로 나눠 가는 방향으로 선택을 하게 되었습니다.
하지만 분리했을때의 단점으로는 비교적 구현이 복잡할 것 같다는 단점이 있었고, 주문 시점에 재고를 감소시키다 보니까 결제 시 결제가 실패하면 재고를 롤백하는 보상 트랜잭션을 따로 구현해줘야 하는 구현 복잡성이 존재했습니다.
그리고 결제가 실패해도 이전 트랜잭션 단위인 주문 시점에 주문과 주문 아이템 엔티티가 테이블에 생성되기 때문에 불필요한 데이터가 생성되는 문제도 있었다.
주문 트랜잭션
주문 시퀀스 다이어그램은 아래 사진과 같습니다.
client -> 주문 요청 -> 주문 엔티티 생성 (이때 주문 상태는 "대기") -> 주문하려는 상품의 재고 검사 후 재고 차감 (재고 부족 시 주문 상태를 "실패"로 변경 후 리턴)-> 주문 아이템 엔티티 생성 -> 주문 엔티티 상태 "주문 완료"로 변경
결제 트랜잭션
결제 시퀀스 다이어그램은 아래와 같습니다.
client -> 결제 요청(주문 정보 전달) -> 주문 상태 검사 ("주문 완료"가 아니면 예외 처리) -> 사용자 잔액 검사 (잔액 부족 시 주문 상태 "결제 실패"로 변경 후 주문 상품 재고 롤백(보상 트랜잭션) 하고 결제 실패 (결제 엔티티 생성 x)) -> 잔액 차감 -> 주문 상태 "결제 완료"로 변경 -> 결제 엔티티 생성
하지만 주문 결제 트랜잭션을 분리하여 구현한 flow에는 허점이 존재했습니다.
1. 재고 차감을 주문 시점에 하게 되면 GMV(Gross Merchandise Volume)가 떨어지는 문제
주문을 해놓고 결제를 하지 않으면 해당 주문 수량 만큼 상품이 팔리지 않게 되니까 매출에 영향을 미치기 때문입니다.
그래서 재고 차감 시점을 주문하는 시점이 아닌 결제 할때 결제에 성공하면 재고를 차감하는 방향으로 수정이 필요했습니다.
2. 결제 시점에 잔액이 부족하여 결제가 실패하면 결제가 되지 않은 상태("주문 완료")로 남는 주문 데이터를 어떻게 처리할 것인지에 대한 문제
주문 결제 동일 트랜잭션
위와 같은 문제로 주문 결제를 한 트랜잭션 단위로 묶어 구현하는 방안으로 재설계하여 서비스를 수정했습니다.
주문 결제 시퀀스 다이어그램
Client -> 주문 요청 -> 주문 생성 (주문 상태 "대기") -> 주문 아이템 생성 -> 주문 상태 "결제 대기" 변경 -> 주문 상태 검사 -> 잔액 검사 -> 잔액 차감 -> 주문 상태 "결제 완료" 변경 -> 결제 생성 -> 주문 상품 재고 검사 -> 재고 차감 -> 주문 및 결제 값 리턴
위와 같은 flow로 하나의 트랜잭션안에서 주문과 결제를 진행하는게 확실히 트랜잭션을 나누는것보다 구현이 비교적 간단했습니다. (보상 트랜잭션을 따로 구현해 줄 필요도 없고..)
그런데 OrderService에서 의존하고 있는 컴포넌트가 너무 많다는 문제가 발생했습니다. (e.g. ProductReader, ProductUpdater, UserPointManager, OrderValidator 등등)
이는 OrderService의 응집도가 매우 낮아지는 문제가 발생하는데, 이 부분에 대해서는 facade 패턴을 적용하여 리팩터링을 해 볼 예정입니다.
결론
설계 초기에 트랜잭션 한 단위로 가져갈 때 예상되었던 문제인 락을 오래 잡는 문제는 주문 결제 트랜잭션을 하나의 트랜잭션으로 구현해 본 뒤 실제로 부하 테스트를 진행 해보고 별도의 트랜잭션으로 나누는 방법도 고려해 보는 걸로 결론을 내렸습니다.
추가로 Spring의 ApplicationEventPublisher를 이용하여 주문 결제를 MSA 스럽게 처리하는 방법도 코치님께 추천받아서 시간 내어 구현해볼 예정입니다.
'성장이야기 > TIL' 카테고리의 다른 글
[E-commerce] 주문 결제를 이벤트 기반 아키텍처로 구축하기 (0) | 2024.04.16 |
---|---|
[E-commerce] Facade Pattern을 사용하여 시스템 응집도와 재사용성을 어떻게 개선할 수 있을까? (0) | 2024.04.11 |
Domain과 Entity의 두 얼굴? (0) | 2024.03.31 |
Actions Runner Controller 를 이용해 self-hosted runner로 배포하기 (0) | 2024.03.19 |
AWS EKS “instances failed to join the kubernetes cluster” Error (0) | 2024.03.12 |