← Ascendy EN

frontend

'동기화 완료'라는데 0장 올라갔다 — 조용한 성공이 가장 위험하다

· Ascendy Engineering


TL;DR

소스 노트. frontend 팀 인테이크 2건(독립 진단·수렴 / root cause fix·리뷰어 캐치)을 합쳐 정제한 글이다. 내부 식별자·절대 수치·API 경로·DB명은 일반화했다(제품이 사진 라이브러리를 동기화한다는 사실만 공개 도메인). 같은 조용한 실패 결의 INFO 로그가 한 줄도 안 찍히던 Celery 이야기와 이어진다.

증상 — “완료”인데 0장

운영자가 제보했다. “특정 날짜 이후 찍은 사진이 자동·수동 동기화 모두 안 올라온다.” 그런데 화면에는 “동기화 완료” 토스트가 떴다. 에러도, 실패 표시도 없이. 실제로는 한 장도 올라가지 않은 채로.

이게 디버깅에서 가장 까다로운 종류다. 시끄러운 실패는 스택트레이스라도 준다. 조용한 성공은 “다 됐다”고 말하면서 아무것도 안 한다 — 어디서부터 봐야 할지조차 안 알려준다. 이 회귀는 6월 들어 세 번째 라운드였고, 매번 다른 원인 후보로 넘어왔다.

1부 — prod 로그 없이, 코드만 보고 9개 가설

프론트는 결정을 하나 내렸다. 백엔드 prod 로그를 기다리지 말고, 지금 가진 것(코드)으로 진단을 시작한다. 운영자의 adb logcat 캡처를 기다렸으면 차단이 며칠 늦었을 것이다.

그래서 코드만 보고 약 9개 가설을 의심 정도 순으로 ranking했다. 1순위는 둘이었다:

9개 후보가 전부 프론트 단독으로 고칠 수 있는 결함 카테고리에 있었다. 그래서 그 중 셋을 한 PR로 한꺼번에 닫았다 — 병렬을 막는 single-entry 락, 취소 플래그가 모든 백그라운드 리스너를 영구히 침묵시키던 버그 제거, 그리고 마지막 동기화 에러를 UI에 노출.

두 진단이 같은 prod 로그에서 만났다

며칠 뒤, 백엔드가 prod 구조화 로그와 DB 집계를 들고 왔다. 세 가지가 한 번에 결판났다.

  1. 새 사진이 서버에 도달조차 안 했다. 클라이언트가 보낸 목록에 새 항목이 하나도 없었다. 즉 백엔드는 깨끗했고, 문제는 전적으로 클라이언트가 새 사진을 보내지 않는다는 데 있었다.
  2. 라이브러리가 client scan cap을 넘었다. 클라이언트는 성능을 위해 고정 크기 윈도우(상위 일부)만 스캔한다. 라이브러리가 그 cap을 넘어가면, 그 윈도우가 라이브러리 전체를 더는 덮지 못한다. 운영자가 찾던 — 안 올라오던 — 사진이 그 윈도우 밖에 있으면, 클라이언트 스캔에 아예 안 보여서 서버에 보낼 목록에 들어가지도 못한다. prod 증거(라이브러리가 cap을 넘었고, 새 항목이 서버에 하나도 도달 안 함)가 정확히 그 상황을 가리켰다.
  3. 세션 interrupt가 prod에서 관측됐다. 프론트 단독 ranking의 1순위(병렬 race)가 그대로 확인됐다.

프론트가 코드만 보고 세운 가설과 백엔드가 prod 데이터로 확정한 결론이, 같은 두 갈래로 정확히 만났다.

진짜 원인은 race가 아니었다

여기가 이 사건의 핵심이다. 병렬 race는 진짜 버그였지만 사용자의 버그는 아니었다.

운영자에게 race fix를 배포한 뒤 보고가 그대로였다: “여전히 새 사진은 안 올라온다.” 예상한 결과였다. 진짜 버그 하나를 닫는 것과, 사용자가 신고한 그 버그를 닫는 것은 다른 일이다. 둘 다 필요했다.

2부 — root cause fix, 그리고 리뷰어가 잡은 데이터 분포 버그

root cause를 닫는 방법은 명확했다. “최신 N장”이라는 고정 윈도우 대신, cursor로 라이브러리 전체를 청크 단위로 walk하면 된다. 한 번에 한 세션만 열고, 그 세션을 완주한 뒤 다음 청크로. (백엔드는 이미 hash를 idempotent하게 처리하니 백엔드 변경은 0.)

v1을 짜서 리뷰에 올렸다. 그리고 여기서 오늘 가장 의미 있는 일이 일어났다 — 독립 LLM 리뷰어가 결함 하나(D1)를 잡았다: 이 cursor는 tie-safe가 아니다.

cursor의 키를 타임스탬프 하나로만 쓰면, 같은 시각에 청크 크기보다 많은 사진이 몰릴 때 문제가 생긴다. 외부 메신저 앱에서 사진을 일괄로 import하거나, 클라우드에서 한꺼번에 복원하면 — 수백·수천 장이 같은 타임스탬프를 갖는다. 이 “plateau”가 청크보다 크면:

이건 lint도, typecheck도, 기존 단위테스트도 못 잡은 결함이다 — 그 데이터 분포를 모델링한 테스트 없이는 안 보인다. 코드 본문만 봐서는 안 보인다 — “타임스탬프가 unique하다”는 적히지 않은 가정에 의존했고, 그 가정이 깨지는 시나리오를 데이터의 분포를 상상해야 비로소 보인다.

v2 — plateau 가정 자체를 없앤다

리뷰어 지적을 받아 v2로 갔다:

이 fix의 좋은 점은 plateau 크기와 무관하게 forward progress를 보장한다는 것이다. plateau가 청크보다 아무리 커도, 각 청크가 strict-less로 다음 영역으로 정확히 한 칸 밀려나기 때문에 누락이 없다. plateau 크기 가정 자체가 사라진다. (그리고 plateau 시나리오를 명시적으로 거는 단위테스트를 함께 박았다 — 바로 그 데이터 분포를 모델링한 테스트다.)

가져갈 것


저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.


Tags: debugging, silent-failure, code-review, frontend, data-distribution