App Store Connect API 함정 시리즈 1편. ASC API를 스크립트로 몰다 보면, 에러 메시지가 진실을 말하지 않는 지점이 여럿 나옵니다. 그 첫 번째.
앱이 심사에서 리젝됐습니다. 함께 제출된 IAP·구독 6종도 전부 REJECTED가 됐죠. 상품 현지화를 고쳐 다시 올리려고 subscriptionLocalizations를 DELETE 했더니 이게 나왔습니다.
409 STATE_ERROR.INVALID_REQUEST_ENTITY_STATE_INVALID
"Cannot delete localization, version is not editable."version이 editable하지 않다니, 그럼 version을 editable하게 만들면 되겠다 싶었습니다. 새 빌드를 attach해서 appStoreVersion을 PREPARE_FOR_SUBMISSION으로 만들었습니다. 같은 409가 났습니다.
에러가 말하는 "version"은 그 version이 아니다
실제로 잠그고 있던 건 아직 열려 있는 reviewSubmission 껍데기였습니다. state는 UNRESOLVED_ISSUES이고, 상품들이 그 안에 REJECTED item으로 묶여 있었습니다. 앱 버전을 아무리 편집 가능하게 만들어도, shell이 열려 있는 한 상품은 잠긴 채입니다.
해결은 그 shell을 canceled=true로 취소하는 것뿐이었습니다. 취소하는 순간 상품 6종이 REJECTED → READY_TO_SUBMIT으로 자동 복귀했습니다. 현지화 DELETE도 재생성도 필요 없었습니다.
에러 메시지가 가리키는 대상과 실제로 잠근 주체가 다르면 디버깅이 통째로 헛돕니다. 이 경우 저는 다른 상황(첫 구독이 DEVELOPER_ACTION_NEEDED일 때)에서 쓰는 "현지화 DELETE → 재생성" 절차를 먼저 시도했고, 12건 전부 409로 헛돌았습니다. 절차 자체는 맞는 절차였지만 상태가 달랐습니다.
취소 비용도 걱정했는데 이 경우엔 없었습니다. "제출 취소는 심사 큐 순번을 버린다"는 게 일반적인 경고지만 그건 심사 대기 중일 때 이야기입니다. 이미 리젝된 shell은 큐에 없으므로 잃을 순번이 없습니다.
같은 shell이 만드는 두 번째 함정 — 빈 껍데기가 슬롯을 먹는다
제출 shell은 다른 방식으로도 문제를 만듭니다. reviewSubmission을 생성한 뒤 add_item이 실패하면, submission 자체는 READY_FOR_REVIEW 상태로 남습니다. 아이템이 하나도 없는 껍데기죠. 이게 5개 쌓이면 한도에 걸립니다.
CONCURRENT_REVIEW_SUBMISSION_LIMIT_EXCEEDED빈 submission도 한도에 포함됩니다. 그리고 이 상태는 API로 지울 수 없습니다.
canceled=truePATCH →409 Resource is not in cancellable state- DELETE →
403 FORBIDDEN_ERROR
canceled=true는 WAITING_FOR_REVIEW에서만 통합니다. READY_FOR_REVIEW 껍데기는 ASC 웹 UI에서 사람이 "Cancel Submission" 버튼을 눌러야만 없어집니다. 자동화 파이프라인이 자기가 만든 쓰레기를 자기가 못 치우는 구조입니다.
정직하게 — 한도를 오해했었다
- "18개 앱을 동시에 submit하면 6번째부터 막힌다"고 생각했는데 틀렸습니다. 한도는 앱당 in-flight submission 수입니다. 계정 전체가 아닙니다. 서로 다른 18개 앱에 각 1개씩은 전혀 안 막히고, 실제로 18/18 동시 달성했습니다.
- 그래서 껍데기를 발견해도 즉시 사고로 볼 일이 아니었습니다. 33앱 웨이브에서 12개 앱에 껍데기가 23건 남아 있었지만 33/33 create가 전부 성공했습니다. 어느 앱도 5건에 도달하지 않았기 때문입니다. 발견하면 사고로 보지 말고 그 앱의 개수가 5인지만 확인하면 됩니다.
- IAP·구독을 리뷰 제출에 넣는 것은 여전히 API로 불가능합니다.
inAppPurchaseV2·inAppPurchase·subscription관계 전부409 ENTITY_ERROR.RELATIONSHIP.UNKNOWN. ASC 웹 UI의 "심사에 추가" 버튼 전용입니다. 즉 이 파이프라인은 끝까지 자동화할 수 없고, 사람이 브라우저를 열어야 하는 지점이 최소 한 곳 남습니다.
상태별 취소 가능 여부
| state | canceled=true PATCH | DELETE |
|---|---|---|
WAITING_FOR_REVIEW |
가능 | — |
READY_FOR_REVIEW (빈 껍데기) |
409 | 403 |
UNRESOLVED_ISSUES (리젝 shell) |
가능 → 상품 자동 복귀 | — |
껍데기를 안 만드는 법: create → add_item → submit을 한 함수에 묶고, 예외가 나면 그 자리에서 cancel. 빈 submission을 애초에 안 남기는 게 유일한 예방책입니다.
마지막 submit이 500이면 재실행하지 말 것. PATCH submitted:true가 500 UNEXPECTED_ERROR를 받았을 때 도구를 재실행하면, create/add_item은 이미 성공한 상태라 새 reviewSubmission을 또 만들어 껍데기만 늘립니다. 올바른 복구는 그 앱의 state=READY_FOR_REVIEW이면서 items>=1인 제출을 찾아 거기에만 submitted:true를 다시 PATCH하는 것입니다. 일반화하면 — create/add_item/submit 3단계는 개별 재시도가 가능하고, 재시도 지점을 상태로 판별해야 합니다. 명령을 통째로 다시 돌리면 이미 성공한 단계를 중복 실행해 부작용을 남깁니다.
시리즈 예고
이 계열 소재가 더 있습니다 — WAITING 상태에서 빌드만 갈아끼우기(cancel → DEVELOPER_REJECTED → PATCH build), whatsNew 백필(전 로케일을 채워야 item 추가), metadata-only 변경에도 새 빌드 필요, WAITING 중 새 locale CREATE 차단, DEVELOPER_REJECTED 탈출(versionString 재명명). 개별로는 얇지만 시리즈로는 검색 유입이 나올 소재라 이어서 씁니다.
지금 당신이 쫓는 그 API 에러, 메시지가 지목한 그 리소스가 정말 잠긴 주체입니까? 아니면 그 뒤에 열린 채 잊힌 shell이 있습니까?