Apple이 환불을 증거로 정당화하도록 허용하는 시간은 단 12시간뿐입니다. 눈앞의 트랜잭션이 이름을 알 수 없는 고객의 것이라면 이 시간은 무용지물입니다. appAccountToken은 그 이름을 제공하는 UUID이며, 구매가 일어나는 순간에만 연결할 수 있습니다.
알림이 도착하는 순간을 떠올려 보세요. Apple은 이 구매자가 제품을 실제로 얼마나 소비했는지, 고객으로 지낸 기간은 얼마나 되는지, 전체적으로 얼마를 지출했는지 알고 싶어 합니다. 타당한 질문들이고, 여러분의 데이터베이스에는 모든 답이 들어 있습니다. 문제는 눈앞의 페이로드가 제공하는 것이 originalTransactionId와 제품 식별자뿐이며, 둘 다 여러분의 사용자 테이블 어디에도 대응되지 않는다는 점입니다. 여러분은 이름이 적히지 않은 청구서를 손에 쥐고 있고, 필요한 답은 이름별로 정리되어 있습니다.
이 불일치에는 해결책이 있고, 그 해결책에는 기한이 있습니다. appAccountToken은 여러분의 시스템이 생성해 결제 시점에 StoreKit에 전달하는 UUID이며, Apple은 이를 그 결과로 생성되는 트랜잭션에 새겨 넣습니다. 그 시점 이후로 Apple이 해당 구매에 대해 알려주는 모든 정보는 이미 식별이 완료된 상태로 도착합니다. 하지만 이 순간을 놓치면 해당 판매 건은 영구적으로 익명으로 남습니다. 아래에서는 이 필드가 무엇인지, 어떻게 연결하는지, 여러분의 앱이 전혀 보지 못한 구매를 사후에 구제해 주는 엔드포인트, 그리고 익명 환불이 실제로 여러분의 비즈니스에서 얼마를 빼앗아 가는지에 대한 계산을 다룹니다.
핵심 요약
App Store 구매에 UUID를 연결하면 그로 인해 생성되는 트랜잭션은 여러분의 기록 속 특정 사용자 한 명을 곧바로 가리키게 됩니다. Apple은 그 값을 트랜잭션에 유지하며 해당 트랜잭션이 등장하는 모든 곳에서 다시 돌려줍니다.
Apple이 점검하는 것은 오직 형식뿐이며, 그 형식은 UUID입니다. 데이터베이스 id, 이메일 주소, 직접 이어 붙인 문자열을 넣으면 StoreKit은 아무 불평 없이 그 값을 폐기하고, appAccountToken이 있어야 할 자리에 nil을 돌려줍니다.
StoreKit 2는 이를 단일 구매 옵션인 Product.PurchaseOption.appAccountToken(_:)로 노출하며, 여러분의 시스템이 미리 생성해 해당 계정에 등록해 둔 UUID를 전달받습니다.
최초 구매에 이를 연결해 두면 Apple은 동일한 값을 구독 체인 전체에 걸친 모든 갱신, 모든 재시도 결제, 모든 업그레이드에 전파합니다.
2025년부터는 Set App Account Token 엔드포인트를 통해 서버에서 앱 내 흐름이 전혀 보지 못한 구매, 즉 오퍼 코드 사용과 프로모션 구매 등에 태그를 지정할 수 있습니다.
Apple의 CONSUMPTION_REQUEST에 답변할 수 있게 되는 것은 전적으로 이 토큰 덕분입니다. 이 토큰이 없으면 12시간이라는 시간 안에 소비 내역을 상세히 설명해야 하는 바로 그 고객과 환불 건을 연결할 수 없습니다.
식별할 수 없는 환불은 방어할 수 없는 환불입니다. 증거를 가지고 있었던 판매 건에 대해서도 돈이 돌려 나가며, 그 판매를 이행하는 데 이미 소모한 컴퓨팅 자원, API 호출, 지급금도 함께 사라집니다.
이 필드가 실제로 하는 일
Apple은 이 값을 의미 없는 것으로 취급하는데, 바로 그것이 핵심입니다. 결제 시점에 StoreKit에 전달한 UUID는 무엇이든 트랜잭션에 기록되어 무기한 보존되며, Apple은 이를 해석하거나 그것이 무엇을 가리키는지 검증하거나 실제로 유효한 대상에 연결되는지 확인하려는 시도를 전혀 하지 않습니다. Apple의 공식 정의에 따르면 이는 "트랜잭션을 여러분 자신의 서비스에 있는 사용자 계정과 연결하는 UUID"입니다. 해석은 전적으로 여러분의 몫입니다. Apple은 그저 이 값을 보관했다가 요청 시 돌려줄 뿐입니다.
이 필드가 유용해지는 것은 바로 어디서 다시 돌아오는가 때문입니다. 서버 알림은 서명된 트랜잭션 안에 이 값을 담아 전달합니다. App Store Server API에 Get Transaction Info를 요청하면 거기에도 등장합니다. 최초 구매에 이 값이 담겨 있었다면 구독 체인의 모든 후속 갱신에서도 그대로 재현됩니다. 다시 말해, 한 번만 이 값을 연결해 두면 그 고객이 계속 결제를 이어가는 한 계속해서 다시 나타납니다.
UUID가 아닌 다른 값을 전달하면 경고 없이 사라집니다
개발자들은 대개 이 규칙을 어기고 나서야 그 존재를 알게 됩니다. 내부 계정 id, 이메일 주소, 몇 개 필드를 이어 붙인 값처럼 합리적으로 보이는 무언가를 전달해도 StoreKit은 전혀 이의를 제기하지 않습니다. 예외도, 경고도, 구매 실패도 없습니다. 트랜잭션은 그저 appAccountToken이 nil인 채로 반환되고, 여러분이 있다고 가정했던 연결은 애초에 만들어지지 않습니다. Apple 개발자 포럼에는 이런 혼란에 대한 질문이 꾸준히 올라오며, 보통 "appAccountToken is missing in the transaction payload"라는 식으로 표현되는데, 그 원인은 거의 항상 형식 요구 사항을 충족하지 못한 값입니다.
그 요구 사항은 매우 좁습니다. StoreKit 2는 RFC 4122 UUID만 받아들이고 그 외에는 아무것도 받아들이지 않습니다. 따라서 안전한 패턴은 서버에서 UUID를 생성해 계정에 저장해 두고, 다른 어떤 유형의 값도 구매 호출에 도달하지 못하도록 확실히 막는 것입니다.
이것이 위험한 이유는 바로 아무 소리도 내지 않기 때문입니다. 형식이 잘못된 값은 오류도, 판매 실패도 일으키지 않고 오직 연결 고리 자체만 조용히 폐기할 뿐, 나머지는 모두 정상적으로 진행됩니다. 값이 StoreKit에 도달하기 전에 UUID 형식인지 확인하는 코드 한 줄이면 이 실패 유형 전체를 없앨 수 있습니다.
결제 과정에서 토큰 연결하기
구현에 필요한 것은 구매 옵션 하나뿐입니다. 가입 시점, 또는 고객이 처음 결제 화면에 도달하는 시점에 서버에서 UUID를 생성해 계정 레코드에 기록해 두고, 이후 구매할 때마다 동일한 값을 전달하면 됩니다.
옵션 시그니처는 Product.PurchaseOption.appAccountToken(_ token: UUID)이며, 실제 호출은 try await product.purchase(options: [.appAccountToken(token)])와 같은 형태입니다. 완료된 트랜잭션이 여러분에게 도달하는 방식이 App Store Server API를 통해 직접 검증하는 것이든 서버 알림으로 수신하는 것이든 상관없이, 그 UUID는 함께 실려 오며, 이는 고객 식별을 단일 인덱스 조회 하나로 줄여 줍니다.
고객당 토큰 하나, 영구적으로 재사용
트랜잭션마다 새 토큰을 생성하고 싶어지는 것은 흔히 저지르는 실수입니다. 그렇게 하면 이점이 조용히 무너집니다. Apple은 최초 구매에 있던 토큰이 무엇이든 그 체인 내의 갱신, 재시도 결제, 업그레이드 전반에 걸쳐 그대로 재현하므로, 계정당 하나의 영구적인 UUID를 쓰면 고객의 전체 결제 이력이 계속 연결된 상태로 유지됩니다. 값을 바꾸면 각 구매는 서로 분리된 섬이 되어 버립니다. 여기서 중요한 속성은 안정성이므로, 계정이 생성될 때 UUID 하나를 정해 두고 절대 바꾸지 마세요.
여러분의 앱이 전혀 관여하지 않은 구매를 구제하는 엔드포인트
과거에는 두 가지 구매 경로가 이를 완전히 무력화했습니다. 오퍼 코드 사용은 여러분의 앱이 아니라 App Store에서 이루어집니다. 스토어 목록에서 프로모션 앱 내 구매를 구매하는 것도 마찬가지입니다. 두 경로 모두 여러분의 구매 코드를 실행하지 않으므로 토큰을 전달할 시점 자체가 존재하지 않았고, 그 결과 이런 트랜잭션들은 영구히 익명으로 남아 있었습니다.
이러한 한계는 WWDC 2025에서 Set App Account Token 엔드포인트와 함께 끝났습니다. 여러분의 서버가 본문에 UUID를 담아 App Store Server API에 PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken을 호출하면 Apple이 이를 트랜잭션에 기록합니다. 이 기능은 오퍼 코드 사용과 프로모션 구매를 포함한 모든 제품 유형에 적용되며, 이런 방식으로 전송된 값은 트랜잭션이 이미 가지고 있던 토큰을 교체합니다. 따라서 식별 작업은 여러분의 앱이 전혀 관여하지 않았던 구매에 대해서도 전적으로 서버 측에서 사후에 수행할 수 있는 작업이 되었습니다.
토큰을 어디에 연결하는가는 구매가 어떤 방식으로 이루어졌는지에 달려 있습니다:
일반적인 앱 내 구매(in-app purchase)의 경우, 구매 시점에 Product.PurchaseOption.appAccountToken(UUID)를 통해 StoreKit 구매 옵션에 설정합니다.
오퍼 코드 사용(offer code redemption)의 경우, 연결할 앱 내 지점이 없어 사후에 연결해야 하므로 서버에서 Set App Account Token 엔드포인트로 설정합니다.
App Store의 프로모션 앱 내 구매의 경우, 트랜잭션이 여러분의 앱 바깥에서 발생하므로 동일하게 Set App Account Token 엔드포인트로 설정합니다.
구독 갱신의 경우, 아무것도 할 필요가 없습니다. 토큰은 최초 구매로부터 자동으로 상속됩니다.
연결이 끊어지면 매출에 어떤 영향을 미치는가
여기서 핵심은 기록 정리의 깔끔함이 아닙니다. 핵심은 Apple이 환불 과정에서 개발자에게 정확히 하나의 질문을, CONSUMPTION_REQUEST 형태로 던지며, 이에 답하려면 거론되는 고객이 누구인지 알아야 한다는 점입니다.
소모성 제품과 비갱신형 구독에 대한 환불 요청이 이 알림을 트리거하며, 12시간 이내에 Send Consumption Information으로 응답하는 것이 여러분이 해야 할 일입니다. Apple은 한 개인에 대한 구체적인 정보를 기대합니다. 제품 소비 비율, 계정 유지 기간, 평생 지출액, 배송 상태 등입니다. 이 토큰은 요청 자체 내의 한 필드로도 등장하지만, 더 중요한 역할은 구조적인 것입니다. 왜냐하면 이것이야말로 그 알림을 여러분 자신의 데이터베이스 속 한 행으로 바꿀 수 있는 유일한 수단이기 때문입니다. 토큰을 빼 버리면 사용 추적이 아무리 철저해도 그 요청에는 답할 수 없게 됩니다.
아무것도 없이 답하는 대가
식별되지 않은 환불이 도착하면 남는 선택지는 두 가지뿐이며, 둘 다 좋지 않습니다. 빈 소비 응답을 제출하면 Apple은 이를 최소 사용으로 해석해 승인 쪽으로 결정이 기울게 되며, 이는 많이 사용한 고객에게도 그대로 적용됩니다. 대신 추정치를 지어내면 플랫폼 운영사에 허위 정보를 보고하는 셈이 됩니다. 어느 쪽을 택하든, 증명할 수 있었던 판매 건은 결국 환불되고 마는데, 그것도 이미 이행 비용을 지불한 뒤입니다.
뼈아픈 부분은 이행 비용입니다. 판매 가격과 아무런 관련이 없기 때문입니다. 생성 과정에서 소모된 모델 API 호출, 생성된 이미지, 렌더링하고 내보낸 영상, 그 이후 지급된 크리에이터 정산금까지, 그 돈은 모두 배송 시점에 이미 여러분의 계좌를 떠났으며, 고객에게 환불한다고 해서 그 돈이 돌아오지는 않습니다. 이제 이 문제를 확장해 보세요. 한 명의 익명 고객이 반복적으로 환불을 신청합니다. 상습적인 환불 신청자는 자신을 식별할 수 없는 업체를 일부러 노립니다. 그 시점부터 빠뜨린 토큰은 사소한 누락이 아니라 열려 있는 수도꼭지처럼 작동하기 시작합니다.
Apple의 질문은 한 고객의 실제 소비에 관한 것이며, 이에 답하려면 그 고객이 누구인지 알아야 합니다. 토큰이 없으면 증명할 수 있었던 판매 건을 포기하게 되고, 그것을 이행하는 데 이미 투입된 컴퓨팅 자원과 API 비용도 함께 날아갑니다.
Android도 다른 이름으로 같은 방식을 사용합니다
Google Play에서는 setObfuscatedAccountId가 이 역할을 수행하며, 계정 식별자를 구매에 결합해 orders.reviewrefund를 통한 지급거절(chargeback) 검토 시 실제로 조사할 대상이 존재하도록 합니다. 구현 방식은 상당히 다르지만, 근본 원칙은 그대로 적용됩니다. 신원은 트랜잭션 시점에 결합되어야 하며, 그렇지 않으면 분쟁은 나중에 기본값으로 패배합니다. Apple 버전의 이 원칙이 바로 appAccountToken이며, 이는 UUID를 요구합니다.
모든 구매를 식별 가능하게 유지하는 세 가지 방법
계정당 UUID 하나를 생성해 저장합니다. 가입 시점이나 첫 결제 시점에 생성해 고객 레코드에 기록한, 고객당 하나의 영구적인 토큰을 이후 모든 구매에 재사용합니다.
값을 전달하기 전에 확인합니다. 구매 코드 내부에서 그 값이 실제로 UUID인지 검증해, 형식이 잘못된 식별자가 조용히 트랜잭션의 nil 토큰으로 바뀌는 일이 없도록 합니다.
앱 외부에서 이루어진 구매는 소급 처리합니다. 오퍼 코드 사용이나 프로모션 구매에 대한 서버 알림이 토큰 없이 도착하면 Set App Account Token 엔드포인트를 사용해 올바른 토큰을 연결합니다.
이 세 가지를 모두 도입하면 식별 작업은 더 이상 사후에 시도하는 일이 아니게 됩니다. Apple이 여러분에게 건네줄 때 이미 모든 트랜잭션이 갖추고 있는 속성이 되며, 여기에는 환불 요청도 당연히 포함됩니다.
Refund Sensor는 바로 이 전제 위에 직접 구축되어 있습니다. 저희는 각 트랜잭션과 서버 알림에서 appAccountToken을 추출하고, 이를 해당 계정에 이미 기록되어 있는 소비 데이터와 연결한 뒤, 그 구매자를 실제로 설명하는 수치를 사용해 12시간이라는 시간 안에 여유 있게 Apple의 요청에 응답을 제출합니다. 이름을 붙일 수 있는 환불이야말로 이의를 제기할 수 있는 환불이며, 애초에 이의를 제기할 가치가 있는 유일한 종류입니다.
이 규칙들이 문서화된 곳
자주 묻는 질문
여러분의 시스템이 소유하고 StoreKit은 단지 기록만 하는 UUID입니다. 결제 시점에 이 값을 전달하면 Apple이 그 결과로 생성되는 트랜잭션에 기록하며, 그 이후로는 해당 구매가 등장하는 모든 곳에서 다시 나타납니다. 트랜잭션 정보, 그와 관련된 모든 서버 알림, 그리고 같은 체인에서 이어지는 각 갱신에서 말이죠. 실질적인 효과는 환불 요청을 포함해 Apple이 이후에 알려주는 어떤 정보든 여러분 자신의 데이터베이스에서 올바른 행(row)을 가리킬 수 있게 된다는 것입니다.
거의 틀림없이 형식 문제입니다. StoreKit 2가 받아들이는 표준은 RFC 4122이며, 이 테스트를 통과하지 못한 값은 오류 발생 없이 조용히 폐기되어 nil만 남긴 채 구매는 아무 문제 없었던 것처럼 완료됩니다. 내부 레코드 id, 이메일 주소, 여러 문자열을 이어 붙인 값 모두 이런 식으로 실패합니다. 또 다른 가능성은 여러분의 앱이 애초에 해당 구매를 전혀 처리하지 않은 경우로, 보통 오퍼 코드(offer code) 사용이 그 원인이며, 이 경우 토큰을 전달할 여러분의 코드 자체가 실행되지 않습니다.
2025년부터는 가능합니다. Apple은 바로 이런 상황을 위해 App Store Server API에 Set App Account Token을 도입했습니다. 본문에 UUID를 담아 /inApps/v1/transactions/{originalTransactionId}/appAccountToken에 PUT 요청을 보내면 이미 존재하는 트랜잭션에 토큰을 기록하거나 이미 있던 토큰을 교체합니다. 모든 제품 유형에 적용되며, 이 기능은 특히 오퍼 코드와 프로모션 구매처럼 여러분의 앱이 전혀 처리하지 않은 구매를 염두에 두고 설계되었습니다.
예, 예외 없이 그렇습니다. 그 UUID가 여러분 쪽에서 무엇을 가리키는지는 전적으로 여러분의 몫이며 Apple은 이를 전혀 들여다보지 않지만, 값 자체는 UUID 형식을 만족해야 합니다. 이를 충족하지 못하면 StoreKit은 해당 값을 보관하지 않고 트랜잭션에 nil을 남깁니다.
환불이 발생하면 CONSUMPTION_REQUEST가 트리거되며, Apple은 해당 구매자가 제품을 어떻게 사용했는지 설명하는 답변을 12시간 이내에 기대합니다. 이런 답변을 작성하려면 알림 속 트랜잭션을 실제로 조회 가능한 고객 레코드로 변환해야 하는데, 이것이 바로 이 토큰의 역할입니다. 또한 이 토큰은 요청 내의 필드로도 함께 전달됩니다. 이것이 없으면 답변은 공백이거나 지어낸 내용이 될 수밖에 없고, 공백 답변은 대개 환불 승인으로 이어집니다.



