여기서 설명하는 내용은 인앱결제 개발 가이드를 먼저 확인 후 진행하는 것을 추천합니다.
인앱결제에 대한 프로세스를 모르고 개발을 진행하기에 어려운 부분이 있을 수 있기 때문입니다.
서버 개발환경은 아래와 같습니다.
spring boot 2.1.3
gradle
jdk 1.8
인앱결제 검증을 위한 서버의 프로세스는 아래와 같습니다.
1. Client App에서 결제한 영수증 데이터를 서버로 전달
2. 전달받은 영수증 데이터를 각 플랫폼에 검증 요청
3. 검증 결과에 따라서 서버에서 주문정보를 업데이트
4. 주문정보 업데이트 후 Client App으로 결과를 리턴
그렇다면 구글과 애플에서는 인앱결제 영수증 데이터를 어떻게 검증하는지 각각 보도록 합시다.
구글의 검증방식은 구글 클라우드 플랫폼에서 발급받은 엑세스 키를 사용해야 한다.
엑세스키를 사용해서 인증받은 토큰 값을 영수증 검증 시 포함해서 요청해야 한다.
토큰이 없으면 권한이 없어서 차단되기 때문이다.
개발을 진행하면서 순서는 아래와 같았다.
그러면 이제 Spring boot에서 작업을 진행하도록 하겠습니다.
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'
@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();
}
}
purchase.getPurchaseState()
라고 말할 수 있다. 0이면 결제완료 1이면 취소된 주문건이므로 상태 값에 따라서 주문정보를 업데이트 하도록 해야한다.@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;
}
}
애플의 검증방식은 구글과 비교하면 간단하다.
영수증의 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 앱 파트에서 진행을 한다.
다음에는 인앱결제에서 환불이 발생 시 서버 사이드에서 어떻게 처리하는지 다루도록 한다.
access token 은 어떻게 발급받으신 건가요? 찾아도 잘 안나오고 구글 문서는 뒤죽박죽이라 힘드네요