Spring Boot에서 영수증을 검증하자

집 가고 싶다.·2024년 10월 18일

Google play API

목록 보기
2/2

코드 작성

파일 구조 (이전 포스팅에서 받은 .json 키 파일은 resources 폴더 내 아무곳에다 두세요~)

사용된 의존성

의존성의 버전은 안 맞추셔도 됩니다.

	implementation 'com.google.api-client:google-api-client:2.7.0'
    implementation 'com.google.auth:google-auth-library-oauth2-http:1.28.0'
    implementation 'com.google.apis:google-api-services-androidpublisher:v3-rev20241003-2.0.0'
    implementation 'org.json:json:20240303'

📂 File Structure

📂 src/
├── 📂 infrastructure/
│   └── 📂 config/
│       └── 📂 google/
│           └── GoogleConfig.java
├── 📂 purchase/
│   ├── PurchaseService.java
│   └── PurchaseServiceImpl.java
└── 📂 web/
    ├── 📂 controller/
    │   └── PurchaseController.java
    └── 📂 dto/
        ├── 📂 common/
        │   └── CommonResponse.java
        └── 📂 purchase/
            └── PurchaseVerifyGoogle.java

CommonResponse.java

이 레코드는 언제나 공통된 응답을 반환하기 위해서 만들었습니다. 딱히 없어도 됩니다.

/**
 * 요청에 대한 응답을 구조화하는 공통 DTO 클래스입니다.
 *
 * <p>이 클래스는 요청 성공 여부와 관련 데이터를 포함한 응답 객체를 제공합니다.
 * 제네릭 타입을 사용하여 다양한 데이터 타입을 처리할 수 있습니다.</p>
 *
 * @param <T> 응답 데이터의 타입
 * @param success 요청이 성공했는지 여부를 나타내는 플래그
 * @param data 요청에 대한 실제 응답 데이터
 *
 * @version 1.0
 * @since 2024-07-30
 * @author namung08
 */
public record CommonResponse<T>(Boolean success, T data) {
  /**
   * 성공적인 응답을 생성하는 정적 메서드입니다.
   *
   * <p>이 메서드는 주어진 데이터를 포함한 성공적인 응답 객체를 생성합니다.</p>
   *
   * @param <T> 응답 데이터의 타입
   * @param data 응답에 포함할 데이터
   * @return 성공 플래그가 {@code true}인 {@code CommonResponse} 객체
   */
  public static <T> CommonResponse<T> success(T data) {
    return new CommonResponse<>(true, data);
  }

  /**
   * 실패한 응답을 생성하는 정적 메서드입니다.
   *
   * <p>이 메서드는 주어진 데이터를 포함한 실패한 응답 객체를 생성합니다.</p>
   *
   * @param <T> 응답 데이터의 타입
   * @param data 실패에 대한 정보를 담은 데이터
   * @return 성공 플래그가 {@code false}인 {@code CommonResponse} 객체
   */
  public static <T> CommonResponse<T> fail(T data) {
    return new CommonResponse<>(false, data);
  }
}

PurchaseVerifyGoogle.java

구글 영수증을 검증하기 위한 데이터를 body 로 받기 위해서 선언 하였습니다.

public record PurchaseVerifyGoogle(
        String purchaseToken
) {
}

GoogleConfig.java

구글 접근 권한을 전역적으로 관리하기 위해서 만들었습니다.

import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.Collections;

@Slf4j
@Configuration
public class GoogleConfig {
  @Bean
  public GoogleCredentials googleCredentials() throws IOException {
    InputStream inputStream = getClass().getResourceAsStream("resources 폴더내 키 파일 위치 예시 : google/key.json");
    GoogleCredentials googleCredentials = GoogleCredentials.fromStream(inputStream)
            .createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));

    AccessToken accessToken = googleCredentials.refreshAccessToken();
    // Fetch access token
    googleCredentials.refreshIfExpired();

    if (accessToken != null) {
    // AccessToken 이 정상적으로 발급이 되었는지 확인하기 위한 log 입니다. 삭제하셔도 무방
      log.info("Access Token: {}", accessToken.getTokenValue());
      log.info("Token Expiry: {}", accessToken.getExpirationTime());
    }

    return googleCredentials;
  }

  @Bean
  public AndroidPublisher androidPublisher(GoogleCredentials credentials) throws GeneralSecurityException, IOException {
    return new AndroidPublisher.Builder(
            GoogleNetHttpTransport.newTrustedTransport(),
            GsonFactory.getDefaultInstance(),
            new HttpCredentialsAdapter(credentials)
    ).setApplicationName("앱의 패키지명을 입력해 주세요").build();
  }
}

PurchaseService.java

서비스 메서드를 한번에 관리하기 위해서 사용 (JavaDoc 를 사용해 각 메서드의 설명을 작성하면 좋아요)

import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;
import java.io.IOException;
import java.security.GeneralSecurityException;

public interface PurchaseService {
	  // 영수증 검증
  SubscriptionPurchaseV2 verifyReceiptForGoogle(String purchaseToken) throws IOException, GeneralSecurityException;
}

PurchaseServiceImpl.java

위에서 선언한 메서드를 구현하는 클래스 입니다.

import java.io.IOException;
import java.security.GeneralSecurityException;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.model.SubscriptionPurchaseV2;

@Service
@RequiredArgsConstructor
public class PurchaseServiceImpl implements PurchaseService {
  private final AndroidPublisher androidPublisher;
  
  @Override
  public SubscriptionPurchaseV2 verifyReceiptForGoogle(String purchaseToken) throws IOException, GeneralSecurityException {
    AndroidPublisher.Purchases.Subscriptionsv2.Get request = androidPublisher.purchases().subscriptionsv2().get("kr.co.july.hanstyle", purchaseToken);
    return request.execute();
  }
}

PurchaseController.java

import co.kr.hanstyle.purchase.service.PurchaseService;
import co.kr.hanstyle.web.dto.common.CommonResponse;
import co.kr.hanstyle.web.dto.purchase.PurchaseVerifyGoogle;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.security.GeneralSecurityException;

@RestController
@RequestMapping("/api/v1/purchase")
@RequiredArgsConstructor
public class PurchaseController {
  private final PurchaseService service;
  
  @GetMapping("/verify/google")
  public ResponseEntity<?> purchaseVerify(@RequestBody PurchaseVerifyGoogle purchaseVerifyGoogle) throws GeneralSecurityException, IOException {
    return ResponseEntity.ok(CommonResponse.success(service.verifyReceiptForGoogle(purchaseVerifyGoogle.purchaseToken())));
  }
}
profile
틀린거 있으면 알려주세요.

0개의 댓글