배포와 인프라8 분 읽기

`version not editable`의 주체는 버전이 아니었다

심사 리젝 후 상품 현지화를 고치려는데 409 'version is not editable'가 났습니다. 그래서 버전을 편집 가능하게 만들었더니 같은 409. 에러가 말하는 'version'은 appStoreVersion이 아니라, 아직 열려 있는 reviewSubmission 껍데기였습니다. App Store Connect API 함정 시리즈 1편.

#app-store-connect#api#ios#gotchas
개념 도식: reviewSubmission 껍데기가 REJECTED 상품 6개를 가두고 있고, 밖에서 appStoreVersion을 PREPARE_FOR_SUBMISSION으로 바꾸는 화살표가 튕겨 나온다. 껍데기를 취소하는 화살표 하나만 안쪽에 닿아 상품이 자동 복귀.
에러가 가리키는 대상과 실제로 잠근 주체가 다르면 디버깅이 통째로 헛돈다.

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종이 REJECTEDREADY_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=true PATCH → 409 Resource is not in cancellable state
  • DELETE → 403 FORBIDDEN_ERROR

canceled=trueWAITING_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:true500 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이 있습니까?

관련 글