Apple App Store 구독 결제 구현하기
Apple App Store에서 자동 갱신 구독을 판매하고 사용자에게 구독 혜택을 지급하려면 아래 절차를 순서대로 완료하세요.
시작하기 전에 공통 사전 준비를 마치세요.
전체 흐름
구독은 소모성 상품과 달리 첫 결제 이후에도 주기마다 갱신됩니다. 갱신 결제는 앱을 거치지 않고 App Store에서 Hive Axyl 서버로 통지되므로, 앱 서버가 그 통지를 받을 준비를 해 두어야 합니다. 첫 결제의 영수증 검증과 구독 혜택 지급은 8단계에서 앱 서버가 Hive Axyl 서버 API로 처리합니다.
| 순서 | 구분 | 하는 일 |
|---|---|---|
| 1 | 스토어 콘솔 | App Store Connect에서 인증 키 발급과 구독 상품 등록 |
| 2 | Hive 콘솔 | 인증 정보 등록, 구독 상품 등록, 구독 서버 알림 URL 등록 |
| 3 | Hive Axyl SDK, 스토어 플러그인 | SDK 모듈과 Apple 결제 플러그인 설치 및 등록 |
| 4 | 레시피 코드 | 레시피 폴더 복사 |
| 5 | 레시피 코드 | PurchaseOrder 생성 |
| 6 | 레시피 코드 | 구독 시작 |
| 7 | 레시피 코드 | 구독 구매 정보 저장 |
| 8 | 앱 코드, 앱 서버, Hive Axyl 서버 API | 영수증 검증과 구독 혜택 지급 |
| 9 | 레시피 코드 | 구독 확정 |
| 10 | 앱 서버 | 자동 갱신 통지 처리 |
| 11 | Hive 콘솔, Hive Axyl 서버 API | 구매 내역 확인 |
9단계를 미루지 마세요
확정하기 전까지 StoreKit은 해당 트랜잭션을 완료되지 않은 것으로 보고 앱을 실행할 때마다 다시 전달합니다. 혜택 지급이 끝나는 대로 9단계를 호출하세요.
1. App Store Connect 설정
-
스토어 콘솔 App Store Connect에서 설정합니다.
상세 절차: Apple 콘솔 결제 연동
App Store Connect에서 Private Key와 Private Key ID, Issuer ID를 발급받고, 판매할 자동 갱신 구독 상품을 등록하세요.
2. Hive 콘솔 설정
- Hive 콘솔 Hive 콘솔에서 설정하거나 확인합니다.
1단계에서 받은 값을 Hive 콘솔에 등록하고, 구독 상품과 갱신 통지를 받을 URL을 등록하세요.
| 설정 항목 | 필수 여부 | 확인할 곳 |
|---|---|---|
| 인증 키 등록 | 필수 | App Store 인증 정보 |
| 상품 유형을 구독으로 지정해 상품 등록 | 필수 | 상품 정보 등록 |
| 구독 서버 알림 URL 등록 | 필수 | 서버별 알림 URL 등록 |
구독 서버 알림 URL은 갱신과 해지, 환불 통지를 앱 서버가 받는 통로입니다. 등록하지 않으면 첫 결제 이후의 상태 변화를 앱 서버가 알 수 없습니다.
3. SDK 모듈과 플러그인 준비
-
Hive Axyl SDK Hive Axyl SDK를 앱에서 호출합니다.
스토어 플러그인 스토어 결제 플러그인을 앱에서 호출합니다.
상세 절차: 모듈 설치, 초기화, 로그인, 플러그인 등록
필요한 SDK 모듈과 Apple 결제 플러그인을 설치한 뒤 SDK를 초기화할 때 등록하세요. 소모성 상품 결제와 같은 구성입니다. 결제는 로그인한 사용자를 기준으로 동작하므로 결제 모듈과 함께 인증 모듈 AddAuth()와 토큰 모듈 AddToken()도 등록해야 합니다. 결제 모듈을 등록하지 않으면 레시피가 FailedPrecondition 오류로 실패합니다.
아래는 SDK를 초기화할 때 인증, 토큰, 결제 모듈과 Apple 결제 플러그인을 등록하는 예제 코드입니다. 예제는 개발과 테스트를 위해 결제 모듈을 샌드박스 서버에 연결하므로, 실제 서비스 빌드에서는 sandbox: true 없이 AddPayments()로 등록하세요.
using Hive.Axyl.Core;
using Hive.Axyl.Core.Unity;
using Hive.Axyl.Auth;
using Hive.Axyl.Payments;
using Hive.Axyl.Payments.Addon.Apple;
var config = CoreConfig.CreateBuilder("{appId}").Build();
HiveBootstrap.Initialize(config, builder =>
{
builder.AddAuth();
builder.AddToken();
builder.AddPayments(sandbox: true);
builder.AddStoreKit();
});
4. 레시피 코드 설치
- 레시피 코드 레시피 코드를 프로젝트에 복사합니다.
레시피는 패키지가 아니라 프로젝트에 복사해서 사용하는 소스 코드입니다. 소모성 상품 결제와 같은 폴더를 사용합니다.
- 복사 대상
- Recipes.asmdef: 공통 어셈블리 정의
- AssemblyInfo.cs: 내부 헬퍼를 다른 레시피 어셈블리에 공개하는 설정
- Helper/: 여러 레시피가 함께 사용하는 공통 코드
- Payments/: 레시피 본체와 공통 타입
- Payments.Apple/: Apple App Store 담당 코드
앱 코드에서 레시피를 호출하려면 앱 쪽 어셈블리 정의의 references에 Hive.Axyl.Samples.Recipes, Hive.Axyl.Samples.Recipes.Payments, Hive.Axyl.Samples.Recipes.Payments.Apple을 추가하세요.
5. PurchaseOrder 생성
-
레시피 코드 레시피 코드를 앱에서 호출합니다.
상세 절차: 상품 목록 조회
구독도 소모성 상품과 같은 PurchaseOrder로 시작합니다. 상품 ID와 가격, 통화는 코드에 직접 적지 말고 사용자가 선택한 구독 상품의 조회 결과로 채우세요.
구독 상품도 상품 목록 조회와 같은 순서로 조회하되, 상품 ID는 ListStoreProductIdsAsync() 응답의 ProductSubscriptions에서 가져오고 상품 상세 정보 조회의 ProductType에는 subscription을 지정하세요. 조회 결과의 ProductId, Price, Currency를 그대로 넣고, Price가 비어 있는 상품은 가격을 확인하지 못한 상품이므로 판매하지 마세요.
using Hive.Axyl.Samples.Recipes;
// 앱이 한 번 정하고 절대 바꾸지 않는 네임스페이스 UUID
const string k_Namespace = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
AccountUuid.TryCompute(k_Namespace, playerId, out var accountUuid);
if (product.Price == null)
{
// 가격을 확인하지 못한 상품은 판매하지 않습니다.
return;
}
var order = new PurchaseOrder(
productId: product.ProductId, // 사용자가 선택한 구독 상품
price: product.Price.Value, // 화면에 표시한 가격
currency: product.Currency,
country: "KR",
language: "ko",
storePlayerId: 0L,
serverId: "server01",
iapPayload: "{\"character\":\"A\"}",
appVersion: Application.version,
accountUuid: accountUuid);
구독에서 accountUuid는 생략하지 마세요
자동 갱신 통지에는 사용자 정보가 들어 있지 않습니다. Hive Axyl 서버는 accountUuid로 첫 결제 기록을 찾아 누구에게 혜택을 지급할지 판단합니다. 이 값이 없으면 갱신 때 지급 대상을 복원하지 못합니다. 생성 규칙은 AccountUuid 생성을 참조하세요.
6. 구독 시작
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
사용자가 구독 버튼을 누르면 레시피의 StartSubscriptionAsync()를 호출하세요. 구독 의사를 서버에 기록하고, StoreKit 결제창을 열어 결과를 받는 것까지 처리합니다.
started.Pending에는 영수증과 주문 정보가 담깁니다. 아래 예제 코드처럼 결과 상태를 확인하기 전에 Apple App Store 결제 구현에서 설명한 구매 정보 저장소에 먼저 저장하고, 구독 확정에 성공한 뒤에만 지우세요.
var recipe = new SubscriptionRecipe(new ApplePurchaseSource());
StartSubscriptionOutcome started = await recipe.StartSubscriptionAsync(order, token);
if (started.Pending != null)
{
// 결과 상태와 관계없이 먼저 저장합니다.
await pendingStore.SaveAsync(started.Pending);
}
switch (started.Status)
{
case StartSubscriptionStatus.Success:
// 7단계로 진행합니다.
break;
case StartSubscriptionStatus.UserCanceled:
// 결제창만 닫혔습니다.
break;
default:
Debug.LogError($"{started.FailedStep}: {started.BusinessOutcome} {started.Error?.Message}");
break;
}
수량을 지정하지 마세요
ApplePurchaseSource는 위 예시처럼 인자 없이 생성하세요. Apple App Store는 복수 결제를 지원하지 않으며, 구독은 언제나 1개입니다.
레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| Hive Axyl SDK | PrepareSubscriptionAsync()로 구독 사전 정보 저장 | PrepareSubscriptionAsync() |
| 스토어 플러그인 | PurchaseAsync()로 결제창 호출과 결과 수신 | Apple App Store 결제 요청 |
구독 사전 정보 저장은 소모성 상품과 달리 생략되지 않습니다. 이 기록이 이후 모든 갱신 주기에서 지급 대상을 찾는 기준점이 됩니다.
7. 구독 구매 정보 저장
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
SaveSubscriptionAsync()를 호출해 구독 구매 정보를 Hive Axyl 서버에 저장하고, 앱 서버에 보낼 구매 정보를 준비하세요. 레시피는 이 단계에서 영수증 검증을 요청하지 않으므로, 성공 결과를 받아도 아직 구독 혜택을 지급하면 안 됩니다.
SaveSubscriptionOutcome prepared = await recipe.SaveSubscriptionAsync(started.Pending, token);
if (prepared.Pending != null)
{
// 저장 단계가 돌려준 값으로 저장한 구매 정보를 교체합니다.
await pendingStore.SaveAsync(prepared.Pending);
}
if (prepared.Status != SaveSubscriptionStatus.Success)
{
// 저장한 구매 정보는 지우지 않습니다. 앱을 다시 실행하면 이 값으로 7단계부터 다시 진행합니다.
return;
}
// 8단계에서 prepared.Pending을 앱 서버에 보냅니다.
이미 저장된 구독이라 저장 요청이 거절되더라도 레시피는 성공으로 처리합니다. 저장과 확정 사이에서 앱이 중단되었다가 다시 시도하면 이렇게 거절될 수 있기 때문입니다. 레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| Hive Axyl SDK | PurchaseSubscriptionAsync()로 구독 구매 정보 저장 | 구독 구매 정보 저장 |
8. 영수증 검증과 구독 혜택 지급
-
앱 코드 앱에서 직접 구현합니다.
앱 서버 앱 서버에서 직접 구현합니다.
Hive Axyl 서버 API 앱 서버에서 Hive Axyl 서버 API를 호출합니다.
상세 절차: Hive Axyl Server API로 영수증 검증
영수증 검증과 구독 혜택 지급은 앱 서버가 처리하며, 레시피는 이 단계에 관여하지 않습니다. 앱이 7단계의 prepared.Pending을 앱 서버에 보내면, 앱 서버는 Hive Axyl 서버 API로 구독 영수증을 검증하고 혜택을 한 번만 지급한 뒤 그 결과를 앱에 응답합니다.
8.1. 앱 서버에 검증과 지급 요청
앱에서 앱 서버를 호출해 구독 영수증 검증과 혜택 지급을 요청하세요. 앱 서버에는 prepared.Pending의 Market, AxylReceipt, VerifyToken, OrderId, StoreTransactionId와 Order에 담긴 주문 정보를 보내세요. 앱 서버를 호출하는 방법과 응답 형식은 앱이 정합니다.
예제 코드의 VerifyAndGrantSubscriptionOnceAsync()는 앱 서버를 호출하는 앱의 메서드입니다. 사용 예제에서는 IPurchaseApplication.VerifyAndGrantSubscriptionOnceAsync()가 같은 역할을 합니다. 이 메서드는 앱 서버가 지급 완료로 응답하면 응답에 담긴 구독 상품 ID로 new SubscriptionDeliveryConfirmation(productId)를 만들어 돌려주고, 거절이나 실패로 지급을 확인하지 못하면 null을 돌려주도록 구현하세요.
8.2. Hive Axyl 서버 API로 구독 영수증 검증
앱 서버는 Hive Axyl 서버 API의 POST /payment/v1/subscription/verify를 호출해 구독 영수증을 검증하세요. 요청 헤더 X-App-Id에는 Hive 콘솔에 등록한 App ID를 넣고, 요청을 보낼 주소는 기본 URL을 참조하세요. 호출에 필요한 앱 서버용 액세스 토큰은 토큰 발급으로 받습니다. 요청과 응답의 모든 필드는 구독 상품 영수증 검증을 참조하세요.
요청 필드에는 앱이 보낸 PendingPurchase 값을 아래와 같이 넣습니다.
providerId:APPLEaxylReceipt:AxylReceipt에 담긴 StoreKit 트랜잭션 JWS. 7단계의 구독 구매 정보 저장과 9단계의 구독 확정에도 쓰는 값country,language,currency,serverId,iapPayload,accountUuid,appVersion:Order의 같은 이름 값requestType: 신규 구매를 나타내는1
8.3. 구독 혜택 지급과 지급 완료 응답
앱 서버는 검증 응답을 확인한 뒤 구독 혜택을 한 번만 지급하고 그 결과를 앱에 응답하세요. 앱 서버가 해야 할 일은 아래와 같습니다.
- 검증 응답의 만료 시각
hiveAxylExpiresDate와 구독 상품hiveAxylProductId를 확인합니다. 이전에 지급한 기록이 있어도 지금 유효한 구독인지는 매번 확인합니다.hiveAxylRefundDate에 값이 있으면 환불된 구독이므로 혜택을 지급하지 않고, 이미 지급한 혜택은 회수합니다. - 앱 서버의 지급 기록에서 이미 지급한 주기인지 확인합니다.
hiveAxylDuplicated가true이면 반드시 확인해야 합니다. 이미 지급한 주기라면 3번을 건너뜁니다. - 만료 시각까지 유효한 혜택을 지급하고 지급 완료를 기록합니다.
- 앱에 지급 완료로 응답합니다. 이미 지급한 주기도 지급 완료로 응답해야 앱이 9단계에서 구독을 확정합니다. 9단계의 구독 확정에 쓰도록 검증 응답의
hiveAxylProductId도 함께 돌려줍니다.
지급을 판단할 때 함께 확인하는 응답 값은 아래와 같습니다.
hiveAxylAccountUuidCompare: 결제한 계정과 요청한 계정의 대조 결과.1은 일치,2는 불일치,9는 대조할 수 없음hiveAxylPayload: 5단계에서 전달한iapPayload가 그대로 돌아오는 값
앱이 보낸 값만 믿고 지급하지 마세요
앱 서버는 요청한 사용자의 계정을 직접 인증하고, 구독 상품과 만료 시각은 검증 응답으로 확인하세요. 앱이 만드는 SubscriptionDeliveryConfirmation은 결제를 증명하는 값이 아닙니다. 지급 여부는 언제나 앱 서버가 판단합니다.
9. 구독 확정
- 레시피 코드 레시피 코드를 앱에서 호출합니다.
앱 서버가 지급 완료로 응답한 뒤에 ConfirmSubscriptionAsync()를 호출하세요. 8단계에서 만든 delivery가 없으면 레시피는 구독을 확정하지 않고 Failure를 돌려줍니다. 레시피는 delivery에 담긴 구독 상품 ID로 확정을 요청하며, 이 값이 비어 있으면 5단계 주문의 상품 ID를 사용합니다.
ConfirmSubscriptionOutcome confirmed = await recipe.ConfirmSubscriptionAsync(
prepared.Pending, // 7단계가 돌려준 값
delivery, // 8단계에서 앱 서버의 지급 완료 응답으로 만든 값
token);
if (confirmed.Status == ConfirmSubscriptionStatus.Success)
{
// 구독 확정이 끝났으므로 저장한 구매 정보를 지웁니다.
await pendingStore.DeleteAsync(prepared.Pending);
}
else
{
// 지급은 이미 끝났고 확정만 남은 상태입니다. 저장한 구매 정보를 그대로 두고 지체 없이 다시 호출하세요.
}
레시피는 서버에 구독 확정을 먼저 요청하고, StoreKit 트랜잭션은 나중에 완료합니다. 트랜잭션을 먼저 완료하면 그 구독으로 돌아갈 수 있는 유일한 통로가 사라지기 때문입니다. 레시피가 내부에서 수행하는 작업은 아래와 같습니다.
| 구분 | 호출 | 확인할 곳 |
|---|---|---|
| Hive Axyl SDK | PostSubscriptionAsync()로 서버에 구독 확정 요청 | 구독 완료 처리 |
| 스토어 플러그인 | FinishTransactionAsync()로 StoreKit 트랜잭션 완료 | 거래 완료 처리 |
10. 자동 갱신 통지 처리
- 앱 서버 앱 서버에서 직접 구현합니다.
첫 결제 이후의 갱신과 해지, 환불은 앱을 거치지 않습니다. App Store가 Hive Axyl 서버에 통지하고, Hive Axyl 서버가 2단계에서 등록한 URL로 앱 서버에 전달합니다.
앱 서버는 통지를 받아 구독 상태를 갱신하고 만료 시각이 지난 사용자의 혜택을 회수하세요. 통지에는 사용자 정보가 들어 있지 않으므로, 5단계에서 전달한 accountUuid로 지급 대상을 찾습니다.
11. 구매 내역 확인
-
Hive 콘솔 Hive 콘솔에서 설정하거나 확인합니다.
Hive Axyl 서버 API 앱 서버에서 Hive Axyl 서버 API를 호출합니다.
운영 중에는 구매 내역을 조회해 구독 상태를 확인하세요.
| 구분 | 방법 | 확인할 곳 |
|---|---|---|
| Hive 콘솔 | 콘솔에서 조회 조건을 지정해 확인 | 조회 조건 지정 |
| Hive Axyl 서버 API | 앱 서버에서 구매 내역 조회 API 호출 | 구매 내역 조회 |
실패 처리
레시피 메서드는 예외를 던지지 않고 결과 객체를 돌려줍니다. 언제나 Status부터 확인하세요.
Status | 의미 | 앱이 할 일 |
|---|---|---|
Success | 해당 단계가 정상적으로 끝났습니다. | 다음 단계로 진행합니다. |
BusinessOutcome | Hive Axyl 서버나 마켓이 요청을 거절했습니다. | BusinessOutcome 값에 따라 분기합니다. 값별 대응 방법은 결제 레시피의 거절 사유를 참조하세요. |
Failure | 네트워크 오류나 타임아웃 같은 기술적 실패입니다. | Error를 기록하고 재시도 여부를 판단합니다. |
UserCanceled | 사용자가 결제창을 닫았습니다. | 오류가 아니므로 구독 화면으로 돌아갑니다. |
UserCanceled는 구독을 시작하는 단계에만 있습니다. 저장과 확정 단계의 결과 타입에는 없으므로 그 값으로 분기하면 컴파일되지 않습니다.
실패해도 PendingPurchase를 버리지 마세요
SaveSubscriptionOutcome과 ConfirmSubscriptionOutcome은 실패할 때도 전달받은 Pending을 그대로 돌려줍니다. 돌아온 Pending으로 6단계에서 저장한 구매 정보를 교체해 두고, 8단계에서 앱 서버가 지급을 확인하지 못한 경우에도 구매 정보를 그대로 두세요. 앱을 다시 실행했을 때 확정되지 않은 구독의 구매 정보가 남아 있으면 그 값으로 7단계부터 다시 진행하세요. 이미 저장되었거나 이미 확정된 구독이어도 레시피가 이어서 처리합니다.
구독과 미지급 구매 복구
구독은 Apple App Store 미지급 구매 복구로 처리하지 마세요. 그 복구는 소모성 상품만 다룹니다. 첫 결제 이후의 갱신과 해지, 환불은 10단계의 자동 갱신 통지로 처리합니다.
다음 단계
소모성 상품도 판매한다면 Apple App Store 결제 구현을 참조하세요.