Table of Contents
안드로이드 – 인앱 구독 서비스 구현하기 (Java)
구글 플레이에서의 구독취소 정보를 서버로 전송하는 부분은 누락되어 있습니다.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
SubscriptionViewModel 구현
코맨트 버전
public class SubscriptionViewModel extends AndroidViewModel {
/**
* ViewModel 생성자
* BillingClient를 초기화하고 Google Play 결제 서비스에 연결을 시작합니다.
*
* @param application Application 컨텍스트
*/
public SubscriptionViewModel(@NonNull Application application) {
super(application);
billingClient = BillingClient.newBuilder(application)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
connectToBillingClient();
}
/**
* Google Play Billing 서비스에 연결을 시도합니다.
* 연결이 성공하면 상품 조회와 구독 상태 확인을 수행합니다.
* 연결이 실패하면 재연결을 시도합니다.
*/
private void connectToBillingClient() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
queryAvailableProducts();
checkSubscriptionStatus();
}
}
@Override
public void onBillingServiceDisconnected() {
connectToBillingClient();
}
});
}
/**
* Google Play Store에서 제공하는 구독 상품 정보를 조회합니다.
* 월간 구독과 연간 구독 상품의 정보를 가져옵니다.
* 조회된 상품 정보는 monthlySubscription과 yearlySubscription 변수에 저장됩니다.
*/
private void queryAvailableProducts() {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("monthly_sub")
.setProductType(BillingClient.ProductType.SUBS)
.build()
);
// ... 구현 내용 ...
}
/**
* 현재 사용자의 구독 상태를 확인합니다.
* 구매 내역을 조회하여 유효한 구독이 있는지 확인하고,
* 구독 상태를 LiveData를 통해 UI에 반영합니다.
*/
private void checkSubscriptionStatus() {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build(),
(billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
verifySubscriptionStatus(purchases);
}
}
);
}
/**
* 구매 내역을 검증하고 구독 상태를 업데이트합니다.
* PURCHASED 상태의 구매 건을 처리하고 구독 상태를 LiveData에 반영합니다.
*
* @param purchases 구매 내역 리스트
*/
private void verifySubscriptionStatus(List<Purchase> purchases) {
boolean hasValidSubscription = false;
for (Purchase purchase : purchases) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
hasValidSubscription = true;
handlePurchase(purchase);
}
}
isSubscribed.postValue(hasValidSubscription);
}
/**
* 구독 구매 플로우를 시작합니다.
* 선택된 상품(월간/연간)의 구매 화면을 실행합니다.
*
* @param activity 현재 Activity 컨텍스트
* @param productId 구매할 상품 ID ("monthly_sub" 또는 "yearly_sub")
*/
public void purchaseSubscription(Activity activity, String productId) {
ProductDetails productDetails = "monthly_sub".equals(productId)
? monthlySubscription : yearlySubscription;
// ... 구현 내용 ...
}
/**
* 구매 완료된 상품을 처리합니다.
* 구매 확인(Acknowledge)을 수행하고, 서버에 구독 정보를 전송합니다.
*
* @param purchase 구매 완료된 상품 정보
*/
private void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
// ... 구현 내용 ...
}
}
}
/**
* Google Play Billing의 구매 업데이트 리스너
* 구매 상태가 변경될 때마다 호출되어 구매 처리를 수행합니다.
*/
private final PurchasesUpdatedListener purchasesUpdatedListener =
(billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
checkSubscriptionStatus();
}
};
/**
* 현재 구독 상태를 반환하는 LiveData를 제공합니다.
* UI에서 이 LiveData를 관찰하여 구독 상태 변경을 감지할 수 있습니다.
*
* @return 구독 상태를 나타내는 LiveData
*/
public LiveData<Boolean> getIsSubscribed() {
return isSubscribed;
}
/**
* ViewModel이 제거될 때 호출됩니다.
* BillingClient 연결을 종료하고 리소스를 정리합니다.
*/
@Override
protected void onCleared() {
super.onCleared();
billingClient.endConnection();
}
}
전체소스코드
public class SubscriptionViewModel extends AndroidViewModel {
private static final String TAG = "SubscriptionViewModel";
private final BillingClient billingClient;
private final MutableLiveData<Boolean> isSubscribed = new MutableLiveData<>(false);
private ProductDetails monthlySubscription;
private ProductDetails yearlySubscription;
public SubscriptionViewModel(@NonNull Application application) {
super(application);
billingClient = BillingClient.newBuilder(application)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
connectToBillingClient();
}
private void connectToBillingClient() {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
queryAvailableProducts();
checkSubscriptionStatus();
}
}
@Override
public void onBillingServiceDisconnected() {
// 재연결 로직
connectToBillingClient();
}
});
}
private void queryAvailableProducts() {
List<QueryProductDetailsParams.Product> productList = new ArrayList<>();
// 월간 구독
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("monthly_sub")
.setProductType(BillingClient.ProductType.SUBS)
.build()
);
// 연간 구독
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("yearly_sub")
.setProductType(BillingClient.ProductType.SUBS)
.build()
);
QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();
billingClient.queryProductDetailsAsync(
params,
(billingResult, productDetailsList) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
for (ProductDetails productDetails : productDetailsList) {
if ("monthly_sub".equals(productDetails.getProductId())) {
monthlySubscription = productDetails;
} else if ("yearly_sub".equals(productDetails.getProductId())) {
yearlySubscription = productDetails;
}
}
}
}
);
}
private void checkSubscriptionStatus() {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build(),
(billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
verifySubscriptionStatus(purchases);
}
}
);
}
private void verifySubscriptionStatus(List<Purchase> purchases) {
boolean hasValidSubscription = false;
for (Purchase purchase : purchases) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
hasValidSubscription = true;
handlePurchase(purchase);
}
}
isSubscribed.postValue(hasValidSubscription);
}
public void purchaseSubscription(Activity activity, String productId) {
ProductDetails productDetails = "monthly_sub".equals(productId)
? monthlySubscription : yearlySubscription;
if (productDetails != null) {
List<BillingFlowParams.ProductDetailsParams> productDetailsParamsList =
List.of(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(productDetails.getSubscriptionOfferDetails()
.get(0).getOfferToken())
.build()
);
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build();
billingClient.launchBillingFlow(activity, billingFlowParams);
}
}
private void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(params, billingResult -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// 서버에 구독 정보 전송
sendSubscriptionToServer(purchase);
}
});
}
}
}
private final PurchasesUpdatedListener purchasesUpdatedListener =
(billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
checkSubscriptionStatus();
}
};
public LiveData<Boolean> getIsSubscribed() {
return isSubscribed;
}
@Override
protected void onCleared() {
super.onCleared();
billingClient.endConnection();
}
}
SubscriptionActivity
public class SubscriptionActivity extends AppCompatActivity {
private SubscriptionViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_subscription);
viewModel = new ViewModelProvider(this).get(SubscriptionViewModel.class);
// 구독 상태 관찰
viewModel.getIsSubscribed().observe(this, isSubscribed -> {
if (isSubscribed) {
// 구독 상태에 따른 UI 업데이트
showSubscribedUI();
} else {
showUnsubscribedUI();
}
});
// 구독 버튼 클릭 리스너
findViewById(R.id.btnSubscribeMonthly).setOnClickListener(v ->
viewModel.purchaseSubscription(this, "monthly_sub"));
findViewById(R.id.btnSubscribeYearly).setOnClickListener(v ->
viewModel.purchaseSubscription(this, "yearly_sub"));
}
}
서버 통신
public class SubscriptionApiService {
private final String baseUrl = "your_server_url";
private final OkHttpClient client = new OkHttpClient();
public void sendSubscriptionToServer(Purchase purchase) {
JSONObject json = new JSONObject();
try {
json.put("purchaseToken", purchase.getPurchaseToken());
json.put("productId", purchase.getProducts().get(0));
json.put("purchaseTime", purchase.getPurchaseTime());
RequestBody body = RequestBody.create(
MediaType.parse("application/json"),
json.toString()
);
Request request = new Request.Builder()
.url(baseUrl + "/subscription/verify")
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e("SubscriptionApi", "Error sending subscription data", e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
// 응답 처리
}
});
} catch (JSONException e) {
Log.e("SubscriptionApi", "Error creating JSON", e);
}
}
}