인앱결제 서버 개발 (영수증 검증)

givepro·2022년 5월 24일
3

인앱결제

목록 보기
2/3

여기서 설명하는 내용은 인앱결제 개발 가이드를 먼저 확인 후 진행하는 것을 추천합니다.
인앱결제에 대한 프로세스를 모르고 개발을 진행하기에 어려운 부분이 있을 수 있기 때문입니다.

서버 개발환경은 아래와 같습니다.

spring boot 2.1.3
gradle
jdk 1.8

인앱결제 검증을 위한 서버의 프로세스는 아래와 같습니다.
1. Client App에서 결제한 영수증 데이터를 서버로 전달
2. 전달받은 영수증 데이터를 각 플랫폼에 검증 요청
3. 검증 결과에 따라서 서버에서 주문정보를 업데이트
4. 주문정보 업데이트 후 Client App으로 결과를 리턴

그렇다면 구글과 애플에서는 인앱결제 영수증 데이터를 어떻게 검증하는지 각각 보도록 합시다.


Google

구글의 검증방식은 구글 클라우드 플랫폼에서 발급받은 엑세스 키를 사용해야 한다.
엑세스키를 사용해서 인증받은 토큰 값을 영수증 검증 시 포함해서 요청해야 한다.
토큰이 없으면 권한이 없어서 차단되기 때문이다.

개발을 진행하면서 순서는 아래와 같았다.

  1. Google Paly Console
  • 설정 > API 엑세스 > 서비스 계정 > 새 서비스 계정 만들기
    (신규 계정을 사용하는 이유는 인앱결제로 사용하기 위한 계정은 별도로 있어야 한다는 내용을 확인)
  1. Google Cloud Platform
  • 1번 과정을 통해서 계정을 생성
  • 키 관리 > 키 추가 > 새 키 만들기 (JSON)
  1. 발급받은 JSON Key file을 Spring boot 에서 활용

그러면 이제 Spring boot에서 작업을 진행하도록 하겠습니다.

  • build.gradle
    compile "com.google.api-client:google-api-client:1.33.0"
    implementation 'com.google.auth:google-auth-library-oauth2-http:1.6.0'
    compile 'com.google.apis:google-api-services-androidpublisher:v3-rev20220411-1.32.1'
    implementation group: 'com.google.http-client', name: 'google-http-client-jackson2', version: '1.41.7'
  • GoogleCredentialsConfig.java
    위에서 설명했던 JSON KEY 파일을 인증하는 클래스다.
@Component
public class GoogleCredentialsConfig {
	@Value("${GoogleKeyfilePath}")
    private String googleAccountFilePath;

    @Value("ApplicationPackageName")
    private String googleApplicationPackageName;

    public AndroidPublisher androidPublisher () throws IOException, GeneralSecurityException {
        InputStream inputStream = new ClassPathResource(googleAccountFilePath).getInputStream();
        GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream).createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);

        return new AndroidPublisher.Builder(
                GoogleNetHttpTransport.newTrustedTransport(),
                GsonFactory.getDefaultInstance(),
                new HttpCredentialsAdapter(credentials)
        ).setApplicationName(googleApplicationPackageName).build();
    }
}
  • InAppPurcahseService.java
    • 가장 핵심은 purchase.getPurchaseState()라고 말할 수 있다. 0이면 결제완료 1이면 취소된 주문건이므로 상태 값에 따라서 주문정보를 업데이트 하도록 해야한다.
    • 근데 왜 결제 검증하는데 취소가 되어있는지 의문일수도 있다. 하지만 인앱결제 후 검증하는 과정에서 네트워크 오류 또는 사용자 기기의 오류로 인해 결제 이후 정상적으로 진행이 안될수 있. 이러한 과정에서 사용자가 콘텐츠를 사용하지 않고 환불을 이미 해버린 경우 검증하는 과정에서 그에 맞게 처리를 해야한다.
    • 그래서 나는 API를 만드는 과정에서 주문을 검증하는 API, 결제를 진행하는 API 각각 만들어서 사용하도록 했다. (여기서 말하는 주문은 인앱결제 주문 영수증이다. 영수증 정보는 APP 로컬에 저장함)
@Service
public class InAppPurchaseService {
	...
    
	private fianl GoogleCredentialsConfig googleCredentialsConfig;
	
    ... 의존성 생략 ...
    
    public boolean googleInAppPurchaseVerify(GoogleInAppPurchaseRequest receiptRequest) throws GeneralSecurityException, IOException {

      AndroidPublisher publisher = googleCredentialsConfig.androidPublisher();

      AndroidPublisher.Purchases.Products.Get get = publisher.purchases().products().
              get(receiptRequest.getPackageName(), receiptRequest.getProductId(), receiptRequest.getPurchaseToken());
      ProductPurchase purchase = get.execute();

      // 검증하는데 취소된 결제건인 경우
      if (purchase.getPurchaseState() == 1) {
      	// 결제완료 주문인 경우 해당 주문번호 조회 후 환불 처리           
        throw new IllegalArgumentException("purchase_canceled");
      }
      return true;
  }
}

Apple

애플의 검증방식은 구글과 비교하면 간단하다.
영수증의 Receipt-data를 아래 검증 API 주소를 통해 진행하도록 한다.
운영 검증 시 https://buy.itunes.apple.com/verifyReceipt
테스트 검증 시 https://sandbox.itunes.apple.com/verifyReceipt

@Value("${apple.production.uri}")
private String appleProductionUrl;
@Value("${apple.sandbox.uri}")
private String appleSandboxUrl;

public boolean appleInAppPurchaseVerify(AppleInAppPurchaseRequest receiptRequest) {

    Map<String, String> requestMap = new HashMap<>();
    requestMap.put("receipt-data", receiptRequest.getReceiptData());

    RestTemplate restTemplate = new RestTemplateBuilder().build();
    ResponseEntity<AppleInAppPurchaseResponse> responseEntity = restTemplate.postForEntity(appleProductionUrl, requestMap, AppleInAppPurchaseResponse.class);

    AppleInAppPurchaseResponse purchaseResponse = responseEntity.getBody();

    if (purchaseResponse != null) {
        int status = purchaseResponse.getStatus();

        // status -> 0 이면 정상 처리
        if (status == 21007) {
            // Test 환경이라면 다시 체크
            responseEntity = restTemplate.postForEntity(appleSandboxUrl, requestMap, AppleInAppPurchaseResponse.class);
            purchaseResponse = responseEntity.getBody();
        } else if (status != 0) {
            throw new IllegalArgumentException("apple_receipt_error_" + status);
        }

        return true;
    } else {
        return false;
    }
}

위 코드의 과정은 간단하다. 전달받은 Request의 데이터에서 "receipt-data"를 조회 후 애플 쪽으로 해당 데이터를 포함해서 조회하는 방식이다.

애플 API에서 리턴받는 데이터의 Status를 확인 후 정상이라면 이후 주문 업데이틀 진행하고 그게 아니라면 에러를 리턴하도록 했다.

애플은 에러 메시지에 대해서 각 케이스별로 정리된 개발 문서가 있으니 참고하면 된다.
단 21007로 리턴되는 상태값은 에러가 아닌 테스트 모드(샌드박스 모드)로 진행한 주문건이기때문에 예외로 진행을 해주면 된다. (공식문서 참고)


이 내용에서는 인앱결제 이후 영수증 검증을 처리하는 부분을 다뤘다.
서버 사이드에서는 영수증 검증 후 해당 서비스의 주문 정보를 업데이트 해주는게 주 역할이다.
인앱결제, 인앱결제 완료 후 스토어에 지급 완료 처리 이런 부분은 android, ios 앱 파트에서 진행을 한다.

다음에는 인앱결제에서 환불이 발생 시 서버 사이드에서 어떻게 처리하는지 다루도록 한다.

profile
server developer

1개의 댓글

comment-user-thumbnail
2024년 3월 5일

access token 은 어떻게 발급받으신 건가요? 찾아도 잘 안나오고 구글 문서는 뒤죽박죽이라 힘드네요

답글 달기