MSA 환경의 결제 서버 운영과 정기 결제 추가하기 — 1

hyunjoon Park
Riiid Teamblog KR
Published in
8 min readJul 21, 2022

안녕하세요 뤼이드 백엔드 개발자로 근무하고 있는 박현준 입니다.

뤼이드에는 ‘산타토익’으로 잘 알려진 AI 기반 개인화 맞춤 학습 서비스가 있습니다. 이러한 비즈니스 요구사항의 구현체인 도메인 서비스의 내부에는 MSA로 만들어진 여러 서비스들이 있는데요!

각 도메인 서비스별 독립성을 강화할 수 있도록 인증, 결제, 쿠폰 등 다양한 서비스에서 사용될만한 공용 서비스들은 마이크로 서비스로 분리해 운영하고 있습니다.

그 중 오늘 이야기 해볼 내용은 결제 서버, 그 중에서도 정기 결제에 관련된 내용입니다. 클라이언트와 도메인 서버를 거쳐 결제 서버까지 어떻게 정기 결제가 진행되는지를 알아보겠습니다.

등장하는 Entity 에 대한 설명

먼저, 이번 주제에서 나올 주체들은 아래와 같습니다.

  1. 클라이언트 : 유저가 실제로 인터렉션을 일으키는 주체
  2. 도메인 서버 : 상품 및 권한을 저장하는 주체
  3. 결제 서버 : 결제 정보를 검증하고 관리하는 주체
  4. 결제사(Ex: App Store) : 유저가 접속해 결제를 진행하는 주체

이 포스트에서는 결제사를 App Store 로 예시를 들어 설명해드리겠습니다.

결제 서버의 목표 소개

결제 서버는 오직 “결제” 정보에 대해서만 관리하는 주체입니다. 따라서 결제 서버의 목표는 다음과 같습니다.

  1. 여러 결제 수단을 통해 결제 정보를 생성하고 검증하는 기능
  2. 각 결제에 대한 일관된 히스토리를 유지하는 기능
  3. 이벤트를 수신해 결제 정보를 업데이트하는 기능
  4. 일회성 결제와 정기 결제 기능
  5. 정산 및 결제 보고서 기능 (TBD)

결제 서버는 위의 목적을 달성하기 위해 다음과 같이 모델들을 구상했습니다.

  1. Subscription Payment : 구독형 결제를 나타내기 위한 모델. Subscription Event History 에 의해 변경된 가장 최신화된 정보.
  2. Subscription Payment Event History : 구독형 결제의 상태가 변경될 때마다 이를 이벤트로 기록하는 모델. 예를 들어 구독형 결제의 갱신이 일어날 경우 해당 엔티티가 추가됩니다.
  3. Payment Queued Event : 스토어로부터 이벤트를 수신했으나, 이에 해당하는 SubscriptionPayment가 존재하지 않을 경우 이벤트를 JSON 상태로 저장합니다.

정기결제의 대략적인 플로우 & 식별자

본격적인 이야기를 하기 전에 정기 결제가 어디서 일어나는지를 설명해드리겠습니다.

정기 결제가 발생하는 곳은 “결제사” 입니다.

즉, 결제 서버에서 결제에 대한 이벤트를 발생시키는 것이 아니라 결제사로부터 정보를 받아와야 합니다.

주로 결제사 쪽에서는 이런 이벤트를 수신하는 방법을 폴링 / 푸쉬 방식으로 제공을 합니다.

이번 예시에서 쓰일 App Store 같은 경우는 푸쉬(웹훅) 방식을 통해서 이벤트를 수신할 예정입니다.

해당 이벤트를 식별하는 정보들이 이벤트와 같이 전달됩니다.

Play store 에서는 클라이언트에서 결제 시 purchaseToken 이라는 값이 전달되고, 결제 서버에서는 이 값을 통해 결제가 실제로 올바르게 이뤄졌는지를 검증합니다. 이벤트에 대한 식별자는 order_id 라는 명칭으로 사용됩니다.

App Store 에서는 클라이언트에서 결제 시 original_transaction_id 와 transaction_id 값을 전달합니다. 결제 서버에서는 이 값을 통해 결제가 실제로 올바르게 이뤄졌는지를 검증합니다. 이벤트에 대한 식별자는 transaction_id 라는 명칭으로 사용됩니다.

이처럼 실제 명칭은 다르지만, 결제 정보를 다루는 식별자라는 의미기 때문에 transaction_id 라고 지칭하고 있습니다. 이번 포스트에서는 결제의 식별자를 transaction_id 라고 일컫겠습니다.

3단계로 이루어지는 정기 결제 플로우

본격적으로 정기 결제가 구매되는 플로우에 대해 설명드리겠습니다.

왼쪽부터 주체는 클라이언트, 도메인 서버, 결제 서버, App Store 입니다.

1. 상품 조회 및 주문 생성

유저가 처음에 상품을 조회하고, 결제를 시도하려고 하는 시점의 플로우입니다.

  1. 클라이언트가 도메인 서버에게 상품 정보를 요청합니다.
  2. 클라이언트가 도메인 서버로부터 상품 정보를 받아옵니다.
  3. 클라이언트가 도메인 서버에게 주문 정보를 생성합니다.
  4. 도메인 서버가 결제 서버에서 결제 정보를 생성합니다. 결제 서버는 결제 정보를 생성합니다.
  5. 결제 서버는 결제 정보를 도메인 서버에게 반환합니다.
  6. 도메인 서버는 주문 정보를 생성합니다. 이후 주문 정보를 클라이언트에게 반환합니다.

2. 클라이언트의 실 결제 이후 결제 검증

유저가 주문 정보를 생성한 뒤 실제 결제가 일어나고 검증하는 플로우입니다.

  1. 클라이언트가 앱스토어에게 실제 결제 요청을 합니다.
  2. 앱스토어에서 실제 결제한 이후에 발행된 transaction_id 를 클라이언트에게 반환합니다.
  3. 클라이언트가 transaction_id 를 도메인 서버에게 전달해 주문 검증을 요청합니다.
  4. 도메인 서버가 transaction_id 를 결제 서버에게 전달해 결제 검증을 요청합니다.
  5. 결제 서버가 transaction_id 라는 식별자를 통해 App Store에 질의합니다. 응답된 결과를 통해 기존 결제 서버에 있는 정보와 결제 정보가 일치하는지 검증합니다.
  6. 검증에 성공했다면 transaction_id 를 기존 결제 정보에 저장해 갱신한 뒤 반환합니다.
  7. 검증된 결제 정보를 통해 도메인에서 유저에게 권한을 부여합니다.
  8. 도메인 서버가 주문 정보를 클라이언트에게 반환합니다.

3. 이벤트 전송을 통한 결제 갱신 및 전파

유저의 상품 구매 이후, 갱신되는 이벤트를 결제 Provider 로부터 전달받아 이를 통해 결제 정보를 갱신하고, 도메인 서버에게 전파하는 플로우입니다. 정기 결제를 통해 발생되는 이벤트들을 수신하고, 갱신하는 역할을 하고 있습니다.

  1. App Store 측에서 유저의 결제 정보가 변경이 일어날 때마다 이벤트를 전송해줍니다. 이벤트의 종류들은 구매, 갱신, 결제 보류 등 다양합니다. 이 때, transaction_id 를 포함해 전달해주기 때문에 결제 서버에서는 이를 통해 어떤 결제 정보가 갱신되었는지 식별할 수 있습니다.
  2. 이벤트를 수신한 결제 서버는 transaction_id 를 통해 기존에 저장된 결제 정보를 검색합니다.
  3. 결제 정보를 찾았다면, 전송된 이벤트에 맞게 결제 정보를 갱신합니다.
  4. 이후 갱신된 결제 정보를 도메인에게 전파하기 위해 Rabbit MQ 에 메시지를 발행합니다.
  5. 도메인 서버에서 발행된 메시지를 구독합니다.
  6. 전달된 결제 정보를 통해서 상품을 갱신합니다.

실패 케이스를 위한 방어책, billId

그런데, 항상 결제가 이렇게 일어나지 않습니다.

2, 3번 플로우에서 문제가 일어나는 상황을 한 번 가정해보겠습니다.

2. 클라이언트의 실 결제 이후 결제 검증

만약 2–3번 과정에서 통신이 끊기면 어떤 일이 일어날까요? (응답이 안 오거나, 타임 아웃이 발생하거나, 유저가 앱을 꺼버리거나 등등 )

유저가 돈은 냈는데, 결제 서버와 도메인 서버는 응답을 받지 못해서 권한을 부여하지 못하게 됩니다.

💡 아까 App Store 측에서 “구매”에 대한 이벤트를 전송해준다고 하지 않았었나요? 그러면 그걸 이용해서 결제 정보를 갱신하고, 이벤트를 도메인 쪽으로 전파해주면 되지 않을까요?

아쉽지만, 그것도 불가능합니다.

3. 이벤트 전송을 통한 결제 갱신 및 전파

왜냐하면 App Store 측에서 transaction_id 를 담은 이벤트를 전송해도, 결제 서버에서는 transaction_id 를 아예 모르기 때문입니다!

즉, 이런 경우에는 유저와 App Store 사이에서는 결제가 이루어졌으나 ✅

도메인 서버와 결제 서버 입장에서는 결제가 이루어졌는지 모르게 됩니다! 😩

이 경우를 해결하기 위해서, 과연 어떻게 해야 할까요?

궁금하시다면, 아래 2편으로 가시죠!

--

--