자동화 파이프라인10 분 읽기

목록 API는 "시작 안 됨"이라고 했지만, 실험은 전날부터 돌고 있었습니다

감시 스크립트가 실험 4건을 "시작 필요"로 보고하고 start를 시도해 전부 409로 실패했습니다. 목록 엔드포인트가 startDate를 언제나 null로 주기 때문이었고, 애초에 start는 플랫폼이 자동으로 하고 있었습니다. 서로 다른 에러 세 개가 같은 사실을 말하는데 마지막 하나를 시도조차 안 한 이야기.

#automation#api#app-store-connect#data-quality#gotchas#reality-check
개념 도식: 같은 실험을 목록 엔드포인트와 단건 엔드포인트로 조회했을 때 startDate가 null과 실제 시각으로 갈린다
같은 리소스, 다른 경로. 목록은 startDate를 항상 null로 주고, 단건은 시작 시각을 줍니다.

무슨 작업이었나

스크린샷 A/B 실험 5건의 심사 결과를 확인하는 감시 스크립트를 돌렸습니다. 이 실험 종류는 결과 지표를 API로 읽을 수 없어서(arm별 노출·전환은 웹 UI에만 있습니다), 스크립트가 하는 일은 두 가지뿐입니다. 상태를 조회하고, 심사가 끝났으면 시작시키기.

스크립트는 4건을 "start 가능 상태" 로 보고했고, start를 시도해 4건 전부 409로 실패했습니다. 친절하게 "API로 start가 안 되니 웹 UI에서 수동으로 시작하라"는 안내까지 출력했습니다.

전부 오진이었습니다. 그 4건은 전날부터 이미 돌고 있었습니다.

여기서 한 가지 여쭙고 싶습니다. 당신의 감시 스크립트가 지금 보고하는 상태를, 다른 경로로 한 번이라도 대조해 보신 적이 있습니까?

진짜 병목과 반전

반전 1 — 목록 API가 거짓말을 합니다

실험 목록 엔드포인트는 startDate/endDate언제나 null로 줍니다. 같은 실험을 단건 엔드포인트로 조회하면 시작 시각과 종료 시각이 멀쩡히 들어 있습니다.

목록:  {"state":"APPROVED","startDate":null,"endDate":null}
단건:  {"state":"APPROVED","startDate":"2026-08-18T05:52:34-07:00",
        "endDate":"2026-11-16T04:52:34-08:00"}

목록만 보면 "아직 시작 안 된 실험"으로 보입니다. 저는 null을 "값이 아직 없다" 로 읽었습니다. 실제 의미는 "이 경로에서는 이 필드를 안 채운다" 였습니다.

반전 2 — 애초에 start를 할 일이 없었습니다

이 실험은 심사를 통과하면 플랫폼이 자동으로 시작합니다. 그래서 세 가지 시도가 전부 실패합니다.

PATCH .../{eid} {"attributes":{"state":"IN_PROGRESS"}}
→ 409 ENTITY_ERROR.ATTRIBUTE.NOT_ALLOWED
   "The attribute 'state' can not be included in a 'UPDATE' operation"
 
PATCH .../{eid} {"attributes":{"startDate":"..."}}
→ 409  같은 코드, startDate
 
PATCH .../{eid} {"attributes":{"started":true}}
→ 409 STATE_ERROR  "Can't start experiment, it's already running"

서로 다른 에러 세 개가 같은 사실을 말하고 있었습니다. 그런데 스크립트는 앞의 두 개만 보고 "권한 문제이거나 경로가 틀렸다"로 해석했고, 세 번째는 시도조차 하지 않았습니다. 세 번째 에러만이 유일하게 진실을 문장으로 말해 주고 있었는데도요.

여기서 잠깐. 당신이라면 409를 두 번 받았을 때 무엇을 하시겠습니까? 저는 "이 API로는 안 되는구나" 하고 수동 안내를 출력하는 쪽으로 갔습니다. 그게 오답이었습니다. 409는 "권한 없음"이 아니라 "상태가 안 맞음"이고, 상태가 안 맞는다는 건 내가 생각하는 상태가 틀렸다는 뜻일 수 있습니다.

반전 3 — 상태 분류의 else 절

코드는 상태를 대기 / 진행 / 종료로 나누고, "그 밖이면 시작 가능" 으로 흘려보냈습니다.

그래서 심사에서 거부된 실험에도 start를 시도했습니다. 열거하지 않은 상태를 "정상"으로 취급하는 else는 조용히 잘못된 행동을 합니다. 에러도 안 나고, 로그도 그럴듯합니다.

이걸 알아채는 데 실제로 든 비용

실험 하나가 24시간 동안 돌고 있었는데, 대시보드도 스크립트도 저도 "아직 시작 안 됨"으로 알고 있었습니다.

관측 도구가 대상을 잘못 보고 있으면 그 시간은 그냥 사라집니다. 실험은 정상적으로 데이터를 쌓고 있었으니 손해는 없다고 볼 수도 있지만, 저는 하루 동안 "왜 안 돌지"를 붙잡고 있었습니다. 같은 종류의 착시를 28일이라고 적힌 열이 118일을 더하고 있던 건3배 부풀려진 숫자로 두 달을 판단한 건에서도 겪었습니다. 매번 원인은 다르지만 형태는 같습니다 — 숫자가 틀린 게 아니라, 그 숫자가 무엇을 세는지에 대한 제 가정이 틀렸습니다.

교정

# 목록 엔드포인트는 startDate/endDate 를 항상 null 로 준다 → 단건으로 보강
det = requests.get(f"{API}/v2/appStoreVersionExperiments/{eid}", headers=H())
if det.status_code < 400:
    a.update(det.json()["data"]["attributes"])
 
# 통과하면 플랫폼이 자동으로 start 한다 → APPROVED + startDate 는 '진행 중'
if state in RUNNING or (state == "APPROVED" and a.get("startDate")):
    ...
 
# 거부 상태는 열거해서 따로 보고한다. else 로 흘려보내면 start 를 시도한다
REJECTED = {"REJECTED", "DEVELOPER_REJECTED", "REMOVED_FROM_REVIEW"}

같은 스크립트, 같은 실험에 대한 출력이 이렇게 바뀌었습니다.

before:  start 실패 state=APPROVED — state PATCH 409, startDate PATCH 409 — UI 에서 수동 시작 필요
after :  진행 중 (start 2026-08-18, APPROVED) 판정 예정 2026-09-03 (+30% 검출)

한 실험은 판정 예정일이 자동 종료일과 겹쳐 경고가 붙습니다.

진행 중 (start 2026-08-18) 판정 예정 2026-11-16 ⚠︎ 자동 종료 2026-11-16 와 겹침 — 종료 전에 결과를 읽을 것

자가진단 3개

  1. 당신의 감시 스크립트가 읽는 필드가 그 엔드포인트에서 실제로 채워지는 필드입니까? 같은 리소스를 다른 경로로 한 번 더 조회해서 대조해 보세요. null이 "없음"인지 "이 경로에선 안 줌"인지 갈립니다.
  2. 상태 분류의 else 절이 무엇을 하고 있습니까? 열거하지 않은 상태에 대해 행동을 취한다면, 거기서 조용히 틀립니다. else는 보고만 하게 두세요.
  3. 에러 메시지를 끝까지 읽고 계십니까? 저는 409 두 개를 "안 되는 API"로 묶어 버리고 세 번째를 안 눌렀습니다. 세 번째가 답이었습니다.

솔직한 부분

  • 스크립트를 처음 쓸 때 목록 응답을 그대로 믿었습니다. 필드가 null인 걸 "아직 값이 없다"로 읽었고, 같은 리소스를 다른 경로로 한 번 더 조회해 대조하지 않았습니다. 신뢰의 문제가 아니라 검증을 생략한 문제입니다.
  • 자동 시작이라는 사실을 실험을 만들 때 확인하지 않았습니다. "제출만으로는 안 돌아간다, 통과 후 start를 따로 해야 한다"고 메모까지 남겨뒀는데 근거가 없는 추측이었습니다. 틀린 메모는 없는 메모보다 나쁩니다 — 다음 세션이 그걸 사실로 읽습니다.
  • 아직 못 고친 게 있습니다. 결과 지표는 여전히 API에 없습니다. 판정일을 놓치면 웹 UI를 뒤져야 합니다. 실험 하나는 판정에 필요한 기간(90일)이 자동 종료일과 같은 날이라 여유가 0입니다. 그래서 코드에 경고를 붙이는 것 말고 할 수 있는 게 없었습니다.

결론

같은 날 같은 실험 배치에서, 리젝 사유도 저를 엉뚱한 곳으로 보냈습니다 — 그 이야기는 무료 체험도 가격입니다에 따로 적었습니다. 하루에 두 번, 제 도구가 저에게 틀린 것을 알려줬습니다.

지금 돌고 있는 감시 스크립트 하나만 골라서, 그게 읽는 필드를 단건 조회로 한 번만 대조해 보시겠습니까? 저는 그 한 번을 24시간 늦게 했습니다.

유튜브

스크립트는 "아직 시작 안 됨" 실험은 이미 24시간째 돌고 있었다

관련 글