본문 바로가기
프로젝트/Wild Bowl : Zooports

인앱 결제 복구 이슈 해결

by argentdarae 2025. 1. 5.
반응형

개요

Wild Bowl : Zooports 프로젝트에서 빌드 심사를 받을 때 발생했던 이슈를 해결하는 과정을 소개한다.

 

 

내용

배경

애플 앱스토어 정책에 따라 유료 재화를 판매하는 앱은 반드시 결제 상품 복구 기능을 제공해야 한다. 그러나 해당 기능이 없는 초기 빌드 상태에서는 심사를 통과할 수 없었다.

 

따라서 인앱 결제 복구 기능을 구현해야 했다.

 

 

문제점

인앱 결제 기능 동작 시퀀스

 

Unity IAP의 경우 공식 메뉴얼을 참고하며 IAP 모듈은 이미 구현했다.

 

구현된 모듈의 영수증 유효성 검증 로직은 Apple App Store에서 구매 성공 시 반환되는 콜백에 포함된 영수증 데이터를 기반으로 동작한다(Class Product의 receipt 멤버 참고).

 

기존 로직에서는 이 receipt를 Lambda로 전송하여 다음과 같은 절차로 구매를 검증하고 있었다:

  1. Lambda는 받은 receipt에서 appleOriginalTransactionID를 추출한다
  2. 추출한 appleOriginalTransactionID를 DB에서 조회하여 이미 거래된 물품인지 확인한다
  3. 처음 거래된 상품이라면 DB의 Transaction Container에 Product_Id와 Origin_Transaction_Id를 저장한다.
  4. 이후 클라이언트에 영수증 승인 사실을 보고하고, 클라이언트는 DB에 구매 컨텐츠 데이터 저장을 요청한다

평소에는 이 로직이 정상적으로 동작했으나, 사용자 관점에서 다양한 시나리오를 가정하며 QA를 진행하는 과정에서 특정 조건에서 문제가 발생할 수 있음을 확인했다.

문제 상황 발생

 

Lambda에 구매 검사를 요청한 뒤 Apple 측에서 영수증 승인이 이루어졌다고 가정했지만, 사용자가 게임을 종료하거나 서버 연결이 끊어져 DB에 구매 데이터를 저장하지 못하는 상황을 테스트하는 중이었다.

 

이 테스트에서는 영수증 승인 결과를 전달받기 전 네트워크가 끊기거나 앱이 종료되는 상황을 가정했으며, 이로 인해 DB에 구매 컨텐츠 데이터가 저장되지 않아 구매가 완료되지 않은 상태로 남을 수 있음을 확인했다. 즉, 구매는 애플 측에서는 승인된 상태지만, Zooports DB에는 동기화가 이루어지지 않은 것이다.

 

따라서 DB에 Product_Id와 Origin_Transaction_Id를 기반으로 이미 구매가 이루어졌는지 확인할 수 있는 상태로 남았다. 그리고, 앞서 구매 검증 로직에서 밝혔지만 애플측에서 콜백으로 전달받는 receipt에서 appleOriginalTransactionId를 통해 유효성 검사를 진행해야 했다.

하지만 게임을 다시 실행하면 Unity IAP 모듈이 초기화되며 보류 중이던 구매들이 자동으로 Lambda에 영수증을 전송하였고 문제가 발생했다. receipt을 기반으로 유효성을 검사했는데, receipt가 비어있는 것이었다.

// 디버그 결과
args.purchasedProduct.definition.id : null
args.purchasedProduct.transactionId : 2000000599796518
args.purchasedProduct.receipt : ""
args.purchasedProduct.appleOriginalTransactionId : 2000000595163741

 

 

해결

팀은 위 버그 상황을 해결하기 위한 방법을 모색해야 했다.

 

테스트 과정에서 receipt가 비어있는 예외 상황이 발견되었고, 이를 처리하기 위해 사용할 수 있는 데이터는 transactionId와 appleOriginalTransactionId뿐이었다. 그러나 transactionId는 초기화되는 특성을 가지고 있어 복구 트랜잭션의 식별에 적합하지 않았다.

 

결론적으로, receipt가 비어있는 예외 상황에서는 appleOriginalTransactionId를 활용해 복구 트랜잭션을 처리해야 한다. 이를 통해 구매 데이터 동기화를 보장하고 예외 상황에서도 안정적인 처리가 가능하도록 해야 한다.

 

결론과 관련 레퍼런스를 정리해서 다음과 같이 백엔드를 담당하는 사수분께 전달하고, 해당 내용을 기반으로 백엔드에서 전달받은 데이터(receipt 또는 appleOriginalTransactionId)를 검사하고 적절히 분기 처리하기로 했다.

receipt는 특정 상황에서 비어있는 값으로 반환되며, 이를 통해 트랜잭션을 식별하기 어려운 경우가 발생한다.
appleOriginalTransactionId는 구매의 고유성을 보장하는 값으로, 이를 활용하면 복구 트랜잭션의 식별 및 처리가 가능하다.
인앱 결제 기능 시스템 플로우

 

이에 따라, appleOriginalTransactionId를 활용하여 복구 트랜잭션을 처리하도록 구현하였다.

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
    // ...
    
    string receipt = args.purchasedProduct.receipt;

    if (string.IsNullOrEmpty(receipt))
    {
        string appleOriginalTransactionId = args.purchasedProduct.appleOriginalTransactionId;
        
        if (string.IsNullOrEmpty(appleOriginalTransactionId))
        {
            // OriginalTransactionId가 존재하지 않아 구매를 처리할 수 없음
            ConfirmPendingPurchase(args.purchasedProduct.definition.id);
            RetryEvent?.Invoke();
            return PurchaseProcessingResult.Complete; // 트랜잭션 종료
        }
        
        PurchasePending?.Invoke(appleOriginalTransactionId);
    }
    else
    {
        PurchasePending?.Invoke(receipt);
    }

    return PurchaseProcessingResult.Pending;
}

 

위 코드는 receipt가 없는 경우 appleOriginalTransactionId를 사용해 구매를 처리하는 로직이다.

 

transaction 종료

if (string.IsNullOrEmpty(appleOriginalTransactionId))
{
    // OriginalTransactionId가 존재하지 않아 구매를 처리할 수 없음
    ConfirmPendingPurchase(args.purchasedProduct.definition.id);
    RetryEvent?.Invoke();
    return PurchaseProcessingResult.Complete; // 트랜잭션 종료
}

 

appleOriginalTransactionId가 null인 경우, 구매를 처리할 수 없으므로 해당 트랜잭션을 종료하고 Complete 처리했다. 이와 함께, 해당 상황을 UI로 알리기 위해 이벤트를 발행했다.

 

또한, appleOriginalTransactionId가 null인 미지의 버그가 발견되어 백엔드에 잘못된 데이터가 들어갈 가능성을 사전에 차단했다. 여러 문서를 참고했으나 이와 같은 상황에 대한 명확한 주의점을 발견하지 못했다. 혹시라도 null이 나오는 상황이 발생한다면, 그때 빠르게 원인을 파악하고 해결 방법을 도출하여 적용하면 된다고 판단했다.

 

 

마무리

이번 과정에서 애플 스토어에 유료 상품을 출시할 때는 구매 복구 기능이 반드시 필요하다는 점을 알 수 있었다. 발생한 문제를 다양한 관점에서 분석하고 현재 상황에 신속하게 적용할 수 있는 해결 방안을 찾을 수 있었던 좋은 경험이었다.

 

또한, 미지의 버그에 대비한 방어적인 코드를 추가함으로써 로직의 안정성을 강화했다.