파일 구조 (이전 포스팅에서 받은 .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'
📂 src/
├── 📂 infrastructure/
│ └── 📂 config/
│ └── 📂 google/
│ └── GoogleConfig.java
├── 📂 purchase/
│ ├── PurchaseService.java
│ └── PurchaseServiceImpl.java
└── 📂 web/
├── 📂 controller/
│ └── PurchaseController.java
└── 📂 dto/
├── 📂 common/
│ └── CommonResponse.java
└── 📂 purchase/
└── PurchaseVerifyGoogle.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);
}
}
구글 영수증을 검증하기 위한 데이터를 body 로 받기 위해서 선언 하였습니다.
public record PurchaseVerifyGoogle(
String purchaseToken
) {
}
구글 접근 권한을 전역적으로 관리하기 위해서 만들었습니다.
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();
}
}
서비스 메서드를 한번에 관리하기 위해서 사용 (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;
}
위에서 선언한 메서드를 구현하는 클래스 입니다.
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();
}
}
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())));
}
}