최종적 일관성과 유저경험을 고려한 주문&결제 아키텍쳐
리워드 포인트 시스템 시리즈
- 1부 : 최종적 일관성과 유저경험을 고려한 주문&결제 아키텍쳐
- 2부 : 요람에서 무덤까지, 고객 경험 중심의 포인트 시스템 기획
개요
- 외부의 기프티콘 발급 서버와 통신하여 기프티콘을 주문-결제하는 서비스의 아키텍쳐를 설명합니다
- 안정적으로 최종적 일관성(eventual consistency)을 보장하기 위해 사용한 아래의 방법을 설명합니다.
- 주기적 재처리 시도
- 멱등성 처리
- 로그 저장
- 또한 장애 발생 시 유저 경험을 가능한 해치지 않도록 어떤 예외처리를 했는지 설명합니다.
배경
회사에서 ‘리워드 포인트’라는 기프티콘 구매 서비스를 설계하고 구현하는 역할을 맡았습니다. ‘리워드 포인트’란 친구초대, 리뷰 작성, 설문 참여 등의 액션을 한 유저가 자사서비스의 포인트를 지급 받고 지급 받은 포인트를 사용하여 기프티콘을 구매할 수 있는 서비스입니다.
그 중 기프티콘 구매는 ‘기프티쇼 비즈’라는 외부업체에서 제공하는 API를 연동하여 구현했습니다. 이는 외부 서비스를 사용하므로 일반적인 웹서비스에서 제공하는 ‘PG사 연동 주문-결제 서비스’와 마찬가지로 단일 트랜잭션으로 강력한 일관성(strong consistency)을 보장할 수 없는 문제가 있습니다.
제가 실무에서 이커머스 사이트를 운영한 경험과 직접 토이프로젝트를 구현하면서 공부한 지식을 바탕으로, 위 문제를 극복하기 위해 최종적 일관성(eventual Consistency)을 보장할 수 있도록 시스템을 설계했습니다.
이번 글에서는 기프티콘 주문-결제 서비스의 아키텍쳐 개요와 최종적 일관성을 보장하기 위해 어떤 방법론을 사용했는지 설명하고자 합니다.
목표
- 장애 상황에 대비하여 최종적 일관성을 보장할 수 있도록 설계한다
- 장애 상황 시 고객의 불편을 최소화할 수 있도록 유저의 관점을 중심으로 예외처리한다
- 변경, 확장에 용이하도록 객체지향적으로 설계한다
시스템 개요
시퀀스 다이어그램 및 상태도
제가 설계한 기프티콘 주문 시스템은 시퀀스 다이어그램 기준으로 크게 아래 7단계로 진행됩니다.
cf) 주문취소 관련 로직은 생략했습니다.
- (1)쿠폰 주문 요청
- (2)유효성 검증
- 기프티콘을 구매하기 위해 충분한 포인트를 보유 중인지 등 유효성을 검증
- (3)주문 엔티티 생성 및 포인트 차감
- (4)기프티콘 구매 시도
- (5)구매 결과에 따라 엔티티 변경
- 성공 시 주문 엔티티의 상태를 ‘주문 성공’으로 변경
- 재고 부족 등의 이유로 실패 시 주문 엔티티의 상태를 ‘주문 실패’로 변경 및 포인트 복구
- (6)쿠폰 주문 결과 응답
- (7)SMS로 기프티콘 전송
주문 엔티티는 아래 상태도(state diagram)를 기준으로 상태변경이 가능합니다.
주문엔티티의 상태는 유한 상태 기계(finite state machine)으로 반드시 한 번에 하나의 상태만 가질 수 있으며 화살표의 방향대로만 변경 가능합니다.
시퀀스 다이어그램의 이벤트와 관계는 아래와 같습니다.
| 시퀀스 다이어그램의 이벤트 | 상태도의 이벤트 |
|---|---|
| (3)주문 엔티티 생성 및 포인트 차감 | (이벤트x)주문 시작 상태로 엔티티 초기화 |
| (5)구매 결과에 따라 엔티티 변경 - 주문성공 | a)주문성공 |
| (5)구매 결과에 따라 엔티티 변경 - 주문실패 | b)주문실패 |
강력한 일관성을 보장하지 못하는 이유와 문제점
기프티콘 주문-결제 서비스의 주 기능은 자사의 DB에 결과를 저장하고 클라이언트에게 올바른 결과를 응답하는 것입니다. 기프티콘이 실제로 구매되었는지 혹은 구매에 실패했는지 등, 기프티콘의 실제 상태(Source of Truth)를 판단하는 기준은 기프티콘 서버의 응답입니다.
그러나 기프티콘 서버와 자사의 서버는 분산된 환경에서 API를 이용해서 통신하기 때문에 단일 트랜잭션으로 강력한 일관성(Strong consistency)을 보장할 수 없습니다.
또한 강력한 일관성을 보장할 수 없으므로 장애 발생 시 클라이언트에게 즉시 올바른 결과를 응답하기 어렵습니다.
따라서 하술할 방법으로 최종적 일관성을 유저경험을 보장해야 합니다.
발생 가능 장애
시퀀스 다이어그램을 기준으로 발생할 수 있는 구체적인 장애 혹은 버그는 아래와 같습니다.
- (ㄱ)클라이언트 중복 주문 요청
- 발생 시점 : (1)쿠폰 주문 요청
- 상세 : 더블클릭 등 동일한 주문에 대한 요청이 2번 이상 들어올 경우 중복 주문이 생성되는 문제가 발생합니다.
- (ㄴ)기프티콘 구매 요청 실패
- 발생 시점 : (4-1)기프티콘 구매 요청
- 상세 : 네트워크 오류, 기프티콘 서버의 장애 등을 이유로 요청이 실패할 수 있습니다.
- (ㄷ)기프티콘 구매 응답 실패
- 발생 시점 : (4-2)기프티콘 구매 결과 응답
- 상세 : 실제로 기프티콘 구매가 완료되었으나 네트워크 오류, 서버 장애 등을 이유로 인해 서버가 응답을 받지 못할 수 있습니다.
- (ㄹ)엔티티 변경 실패
- 발생 시점 : (5-1)구매 결과에 따라 엔티티 변경
- 상세 : 네트워크 오류, DB 장애 등을 이유로 구매 결과에 따른 엔티티 변경을 실패할 수 있습니다.
- (ㅁ)클라이언트 응답 실패
- 발생 시점 : (6)쿠폰 주문 결과 응답
- 상세 : 서버 응답 지연으로 인한 timeout, 네트워크 오류(모바일의 경우 특히 wifi 등의 이슈 가능)로 인해 클라이언트가 응답을 받지 못할 수 있습니다.
최종적 일관성을 보장하기 위해 사용한 방법
상술한 한계로 강한 일관성을 보장할 수 없습니다. 대안으로 딜레이를 감수하더라도 최종적 일관성을 보장할 수 있도록 구현했습니다. 설명은 시퀀스 다이어그램을 기준으로 합니다.
주기적인 재처리 시도
해결한 장애
- (ㄴ)기프티콘 구매 요청 실패, (ㄷ)기프티콘 구매 응답 실패, (ㄹ)엔티티 변경 실패
상세
- 주기적으로 상태값이 ‘주문시작’ 혹은 ‘취소 시작’인 주문 건에 한하여 주문로직을 다시 실행하여 최종적 일관성을 보장합니다.
중요 구현 사항
- 동시성 문제를 방지하기 위해 ‘(4-1)기프티콘 구매 요청’ 직전에 비관적 락으로 주문 엔티티를 잠궜습니다.
- 동시성 문제를 고려하지 않을 경우, 예를 들어 구매 재시도 요청과 구매취소 요청이 동시에 진행될 경우 실제로는 취소에 성공했지만 ‘주문 성공’ 상태가 최종 상태로 남을 수 있습니다.
- 응집성을 위해 주문 상태 변경 로직을 엔티티 내부에 구현하고 외부에서 메소드를 호출하는 방법으로만 변경 가능하도록 구현했습니다.
예시
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
@Entity public class GifticonOrderEntity { . . . public void complete() { if (this.status != PaymentsStatus.START) { throw new IllegalStateException(); } this.status = PaymentsStatus.COMPLETE; } public void fail() { if (this.status != PaymentsStatus.START) { throw new IllegalStateException(); } this.status = PaymentsStatus.FAIL; } . . .
주문 멱등키
재처리 시도 시점에는 이미 기프티콘 구매가 성공했는지 알 수 없습니다. 따라서 멱등성을 고려하지 않을 경우 중복구매가 발생할 수 있습니다. 이를 막기 위해 기프티콘 서버에 구매를 요청할 때 주문 엔티티의 고유한 값을 멱등키로 전달하도록 구현했습니다.
이번 프로젝트에서 기프티콘을 구매, 발급하기 위해 사용한 ‘기프티쇼 비즈’ 서비스는 멱등키를 기준으로 중복주문을 방지하는 기능을 제공합니다. 멱등키는 대부분의 PG사에서 동일하게 제공하는 기능입니다.
로그 저장
사후 정합성 검증 및 트러블슈팅이 용이하도록 상태 변경 시점에 변경 내역도 저장되도록 구현했습니다.
의도적으로 이벤트 소싱과 replay는 배제하고 단순히 저장 및 목록조회만 가능하도록 구현했습니다. 왜냐하면 아직 저의 이벤트 소싱에 대한 이해도가 낮고 프로젝트의 규모 상 오버엔지니어링이라고 판단했기 때문입니다.
유저 경험을 보장하기 위해 사용한 방법
구체적인 예외 메시지 반환
대응한 장애
- (ㄴ)기프티콘 구매 요청 실패, (ㄷ)기프티콘 구매 응답 실패
상세
- 고객이 혼란을 겪지 않도록 ‘일시적으로 문제가 발생했습니다. 해결 후 빠르게 안내드리겠습니다’ 라는 문구가 화면에 나오도록 구현했습니다.
- 이와 별개로 슬랙 알림을 통해 개발자가 빠르게 장애상황을 인지할 수 있도록 구현했습니다.
재처리 이후 결과 안내
대응한 장애
- (ㅁ)클라이언트 응답 실패
상세
- 재처리가 진행됬을 경우 주문요청 시점과 재처리 완료 시점 사이에 필연적으로 딜레이가 발생합니다.
- 결과적으로 기프티콘을 수령했을 경우는 고객이 즉각 인지가 가능합니다. 반면 재고소진 등의 이유로 기프티콘을 수령하지 못했을 경우 별도의 알림이 없다면 나쁜 유저 경험이 발생할 수 있습니다.
- 따라서 구매 실패 시 재처리 직후 알림을 통해 1)포인트가 반환되었고 2)어떤 이유에서 구매가 실패했는지 고객이 알 수 있도록 구현했습니다.
기타 구현 사항
checkout 멱등키(idempotent key)
해결한 장애
- (ㄱ)클라이언트 중복 주문 요청
상세
- 클라이언트 중복 주문 요청 시 버그가 발생하는 것을 막기 위해 멱등키를 사용했습니다. 구체적인 구현 방법은 아래와 같습니다.
- ‘(1)쿠폰 주문 요청’ 시점에 클라이언트는 request body에 checkoutKey라는 필드를 포함해야 하며 값은 클라이언트가 UUID를 직접 생성하여 할당합니다.
- ‘(3-1)주문 엔티티 생성 및 포인트 차감’ 시점에 checkoutKey를 포함하여 주문 엔티티를 생성합니다. RDB의 checkoutKey 컬럼은 unique 제약조건이 걸려 있습니다.
- 서버는 이미 저장된 UUID를 요청 받을 경우 클라이언트에게 이미 주문이 진행 중임을 응답합니다.
cf)일반적으로 장바구니 엔티티에서 checkout 멱등키를 관리하는 것으로 알고 있습니다. 다만 기획 상 이번 프로젝트에서는 클라이언트에서 UUID를 직접 생성하도록 구현했습니다.
추후 구현 고려사항
이벤트 기반 아키텍쳐 도입 가능
- 이번 프로젝트는 첫 서비스 출시이므로 상대적으로 간단하게 구현했습니다.
- 다만 추후 고도화 시 확장성과 유연성을 고려하여 이벤트 기반 아키텍쳐 도입을 검토할 수 있습니다.
후기
이번 프로젝트 덕분에 주문-결제 시스템을 기획부터 설계, 개발까지 직접 주도할 수 있는 소중한 기회를 누려서 기분이 좋았습니다 ㅋㅋ. 대부분의 개발자가 이미 완성된 기존 시스템을 유지보수하는 경험을 얻는 선에서 그치는 것과 비교하면 큰 행운이 따른 것 같습니다.
전 직장에서 이커머스 서비스의 주문-결제 시스템을 다룬 경험, 토이프로젝트를 통해 주문-결제 시스템을 공부한 것도 큰 도움이 되었습니다. 저는 이런 경험이 켜켜이 쌓여 언젠가 훌륭한 시니어 개발자가 되고 싶다는 간절한 열망이 있습니다. 앞으로도 꾸준히 정진하여 꼭 꿈을 이루고 싶습니다.
다음 글에서는 어떤 기획을 통해 리워드 포인트를 수집하고 사용하도록 유도했는지 설명할 예정입니다.


