서드파티 결제서비스 연동가이드(구글 플레이스토어)

구글에서 엔드포인트 유저의 재화결제에 대한 데이터를 식별하고 핸들링하기 위해서는 구글에서 제공하는 결제관련 API와 RTDN 웹훅을 사용해야합니다.

구글에서는 크게 단발성 결제와 구독형 결제의 두 가지 결제타입을 지원하고 있으며, 아래는 각각의 결제 타입에 따른 프론트엔드와 백엔드의 역할 시퀀스 다이어그램입니다.

  • 프론트엔드 = 클라이언트 = 앱
  • 백엔드 = 서버

단발성 결제 시퀀스 다이어그램

seq1

구독형 결제 시퀀스 다이어그램

seq.png

결제정보 핸들링

위 시퀀스 다이어그램을 살펴보면, 단발성 결제와 구독형 결제 모두 purchaseToken과 유저를 매핑하는 부분은 동일함을 알 수 있습니다.

purchaseToken은 해당 결제에 대한 unique id로, 같은 purchaseToken이 영구히 유지되기 때문에, 추후 환불에 대한 요청에서도 같은 purchaseToken으로 어떤 상품인지를 식별해야 합니다.

각 4번 단계(purchaseToken 유효성 검증)요청을 받는 응답(5번) 예시는 다음과 같습니다.

{
  "kind": "androidpublisher#subscriptionPurchase",
  "startTimeMillis": "1715000000000",
  "expiryTimeMillis": "1717600000000",
  "autoRenewing": true,
  "priceAmountMicros": "5900000",
  "countryCode": "KR",
  "paymentState": 1,
  "purchaseType": 0,
  "orderId": "GPA.1234-5678-9012-34567",
  "purchaseToken": "abcd1234efgh5678"
}

purchaseToken이라는 Unique id가 있는데, 왜 orderId라는 값이 또 필요한 걸까요?

purchaseToken은 같은 결제에 대해서 같은 값을 유지하지만, orderId는 같은 결제여도 다른 청구서면 다른 값을 가집니다.

예를 들면,

purchaseToken orderId
결제시 token_123 GPA.0001-1111-2222-33333
환불시 token_123(동일) GPA.0001-1111-2222-33333..0
purchaseToken orderId
최초구독 token_123 GPA.0001-1111-2222-33333
자동갱신 token_123(동일) GPA.0001-1111-2222-33333..0
자동갱신 token_123(동일) GPA.0001-1111-2222-33333..1
자동갱신 token_123(동일) GPA.0001-1111-2222-33333..2

이런식입니다.

그래서 우리는 아래와 같은 엔티티가 필요하다는 결론을 도출할 수 있습니다.

class PurchaseDataExample {
	
	private String purchaseToken;  // 해당 결제내역의 Unique id
	
	private String orderId;        // 해당 청구서의 Unique id
	
	private Long userId;           // 결제자(유저)의 PK
	
	// 이하 환불인지, 단발성구매인지, 구독인지, 구독연장인지, ... 수량이나 가격, createdAt 등
}

세팅

시작 전에

먼저, Google Play Console 접근 권한과 Google Cloud Console 접근 권한이 필요합니다. 사내 마스터 계정 혹은 초대 권한이 있는 동료에게 초대를 부탁하세요.

Google Cloud Console 접근 권한은 Firebase 초대를 받으면 자동으로 획득합니다.

Google Cloud Console 세팅: 토픽생성 및 권한부여

a1.png

a2.png

Google Play Console

a3.png

토픽 이름은 projects/{project_id}/topics/{topic_name} 포맷을 지켜야합니다.

이후 Send test notification 을 통해 제대로 웹훅이 오는지 확인해보세요.

프로젝트당 웹훅 경로는 단 하나밖에 지정할 수 없습니다. 그 말은, 환불 웹훅이나, 구독 갱신 웹훅, 구독 만료 웹훅을 모두 하나의 컨트롤러로 처리해야 한다는 뜻입니다.

또, 개발서버 테스트용, 실서버용으로 두 개를 구분지을 수 없습니다. 배포단계에서 웹훅 엔드포인트는 실서버 하나만 지정 가능합니다.

웹훅 request 드릴(구글 → 서버)

{
  "message": {
    "attributes": {
      "key": "value"
    },
    "data": "eyJwYWNrYWdlTmFtZSI6ICJjb20uZXhhbXBsZS5hcHAiLCAic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjogeyJub3RpZmljYXRpb25UeXBlIjogMywgInB1cmNoYXNlVG9rZW4iOiAiYWJjZDEyMyIsICJzdWJzY3JpcHRpb25JZCI6ICJwcmVtaXVtX21vbnRobHkifX0=",
    "messageId": "123456789012",
    "message_id": "123456789012",
    "publishTime": "2025-05-13T09:00:00.000Z",
    "publish_time": "2025-05-13T09:00:00.000Z"
  },
  "subscription": "projects/your-project/subscriptions/play-rtdn-sub"
}

웹훅은 위와 같은 포맷으로 옵니다. 우리가 필요한 값은 data입니다.

이 data를 base64로 디코딩하면,

{
  "packageName": "com.example.app",
  "oneTimeProductNotification": {
    "notificationType": 5,
    "purchaseToken": "abc123xyz456",
    "sku": "one_time_001" // 단건구매 전용, 만약 구독결제라면 대신 subscriptionId가 옴
  }
}

위와 같은 json 데이터를 얻을 수 있습니다. 여기의 notificationType을 통해 이게 환불인지, 연장인지를 판단할 수 있습니다.

공식문서에서는 notificationType을 다음과 같이 정의하고 있습니다.

else-if 문이나 switch문으로 내부 로직을 분기처리해주세요.

단건구매

b1.png

구독결제

b2.png

Ref.

REST Resource: purchases.products  |  Google Play Developer API  |  Google for Developers

준비하기  |  Google Play’s billing system  |  Android Developers