App Store 리뷰를 API로 폴링해서 DB에 넣고, LLM으로 답글 초안을 만들고, 별점 기준으로 자동 발송하거나 승인 큐에 넣는 봇을 돌리고 있습니다. 앱 43개가 등록돼 있습니다.
플릿 전체를 점검하다가 리뷰 테이블이 2026-06-22에 멈춰 있는 것을 발견했습니다. 총 62건, 그 뒤로 한 건도 없습니다. 7주입니다.
이 글은 그 뒤로 제가 두 번 틀린 기록입니다.
먼저 하나 여쭙겠습니다. 당신의 파이프라인이 "데이터가 없다"고 말할 때, 그게 정말로 없는 것인지 소스가 안 주는 것인지 구분할 수단이 있습니까? 저는 없었습니다.
1차 오진 — "수집기가 죽었다"
pg_cron이 6시간마다 Edge Function을 부르는 구조입니다. 실패 응답을 찾으려고 net._http_response를 뒤졌습니다.
select status_code, count(*), max(created)
from net._http_response
where created > now() - interval '14 days' and (status_code is null or status_code >= 400)
group by 1;실패 2건. "2주에 2건이면 정상이네, 그럼 왜 안 들어오지"로 갔습니다.
틀렸습니다. 이 테이블 보존은 약 6시간입니다.
select min(created), max(created), count(*) from net._http_response;
-- 2026-08-10 00:20 ~ 06:15, 24건interval '14 days'를 써도 6시간짜리 데이터에 걸립니다. 쿼리는 성공하고 결과도 나오는데 창 자체가 존재하지 않습니다. 에러가 없는 종류의 오독입니다.
같은 자리에 함정이 하나 더 있습니다. pg_cron의 job_run_details.status = 'succeeded'는 net.http_post가 큐에 들어갔다는 뜻입니다. HTTP 응답과는 무관합니다. cron이 전부 succeeded인데 함수는 한 번도 성공하지 않은 상태가 아무 모순 없이 성립합니다.
2차 오진 — "리뷰가 제거됐다"
수집기를 못 믿게 되니 원천을 직접 봤습니다. 제 API 키로 43개 앱의 customerReviews를 전부 읽었습니다.
ASC가 돌려준 총 리뷰 35건
DB에 있는 리뷰 62건
DB에 있는데 ASC에 없는 것 27건
ASC에만 있고 DB에 없는 것 0건마지막 줄이 결정적으로 보였습니다. 수집기가 놓친 건 0건이니 수집기는 무죄고, 27건이 사라진 것이죠. 전부 5★였고, 26건은 이미 답글을 보낸 것이었고, 날짜는 4월 말부터 6월 22일까지 전 구간에 퍼져 있었습니다.
62건 중 60건이 5★이고 전부 한국이라는 분포까지 보고 나서 "애플이 진위 검증으로 제거한 것이 가장 그럴듯하다, 계정 수준 리스크일 수 있으니 확인이 필요하다"고 보고했습니다.
당신이라면 여기서 무엇을 하시겠습니까? 애플에 문의를 넣겠습니까, 쿼리를 한 번 더 짜겠습니까, 아니면 그냥 브라우저를 열어보겠습니까?
실제 답 — 오너가 App Store Connect를 열어봤다
리뷰는 그대로 있었습니다.
제거가 아니라 API가 덜 돌려주는 것이었습니다. 지도 앱 하나로 가능한 조합을 전부 시도했습니다. UI에는 3건이 있습니다.
| 요청 | meta.paging.total |
|---|---|
customerReviews 기본 |
0 |
sort=-createdDate |
0 |
filter[territory]=KOR |
0 |
include=response |
0 |
filter[rating]=5 |
0 |
appStoreVersions/{최근 3개}/customerReviews |
0 / 0 / 0 |
전부 0입니다. 그런데 같은 엔드포인트가 5월에는 그 3건을 가져왔습니다. 봇이 그때 저장한 행이 DB에 남아 있으니 확실합니다. 왜 지금은 안 주는지, API 쪽에서는 알 방법이 없습니다.
이 사고에서 남길 규칙
남은 실질 리스크는 리뷰가 없다는 게 아닙니다. 답글 봇이 같은 엔드포인트를 쓰므로 일부 앱의 리뷰를 못 봅니다. 자동 답글이 켜진 앱 7개에서 리뷰가 답글 없이 방치될 수 있습니다. 사용자 입장에서는 "리뷰를 썼는데 개발자가 무시했다"가 됩니다.
그래서 이렇게 정리했습니다.
- 리뷰 건수·최신성의 ground truth는 ASC UI뿐입니다. API가 0을 돌려줘도 없는 게 아닙니다.
- 평점(별점 수·평균)은 애초에 ASC API에 없습니다.
customerReviews는 글이 달린 리뷰만 반환합니다 — 별점만 남긴 대다수가 빠집니다. 스토어에 실제로 표시되는 숫자는 인증 없는 공개 lookup에서 옵니다. 이 이야기는 따로 뺐습니다 → 미국 설치가 제일 많은데 미국 평점은 0이었다.
curl -s "https://itunes.apple.com/lookup?id=<appId>&country=kr" |
python3 -c "import json,sys; x=json.load(sys.stdin)['results'][0]; \
print(x['averageUserRating'], x['userRatingCount'])"- Edge Function이 진짜 성공했는지 보려면 cron의 succeeded가 아니라
net._http_response를 봐야 하고, 그 창은 6시간입니다. 그보다 긴 기간을 보려면 응답을 따로 적재해야 합니다.
지금 바로 점검할 3가지
- 보존 기간이 짧은 시스템 테이블에
interval '14 days'같은 창을 걸어둔 쿼리가 있습니까?select min(created), max(created)를 먼저 찍어보세요. - 스케줄러의 "성공"이 호출 성공입니까, 응답 성공입니까? 둘이 다른 스케줄러를 쓰고 있지 않은지 확인하세요.
- 당신의 파이프라인에서 "0건"과 "모름"이 같은 칸에 들어가 있습니까? 두 상태가 구분되지 않으면 다음 사람이 반드시 오독합니다.
솔직한 부분
두 번 틀렸고, 두 번 다 자신 있게 보고했습니다. 특히 2차는 "제거"라는 무거운 결론이었는데 근거가 "API가 안 준다" 하나였습니다. API를 ground truth로 가정한 것 자체를 한 번도 의심하지 않았습니다.
반증은 제가 못 찾았습니다. 오너가 브라우저를 열어서 끝났습니다. UI를 한 번 보는 것이 API를 다섯 번 때리는 것보다 빨랐습니다.
원인도 여전히 모릅니다. 키의 역할 제한인지, 엔드포인트 동작 변경인지, 인덱싱 지연인지 구분할 방법이 API 쪽에 없습니다. 그리고 첫 결론도 무효가 됐습니다 — "6-22 이후 새 리뷰 0건"은 같은 못 믿을 엔드포인트에 기댄 문장이니까요. 새 리뷰가 있어도 API가 안 줄 수 있습니다. 지금 상태는 "리뷰가 없다"가 아니라 "모른다" 입니다.
당신의 대시보드에서 가장 오래 0을 유지하고 있는 지표 하나를 떠올려 보세요. 그 0을 소스의 UI에서 직접 확인해 보신 적이 있습니까?