유튜브 채널 하나를 자동 운영하는 봇이 있습니다. 매주 영상을 올리고, 같은 시리즈가 2편 이상 쌓이면 플레이리스트를 만들어 묶습니다. 어느 주 로그 끝에 이게 찍혀 있었습니다.
[playlists] 생성: ... PL9x•••••••••
googleapiclient.errors.HttpError: <HttpError 404 ...
"The playlist identified with the request's playlistId parameter cannot be found.">플레이리스트를 만들고, 응답으로 받은 그 ID로 바로 목록을 조회했더니, 그런 플레이리스트는 없다고 합니다. 방금 당신이 준 ID인데요.
당신의 코드는 외부 API에 뭔가를 만든 직후, 그걸 다시 읽을 수 있다고 가정하고 있지 않습니까?
404는 거짓말이 아니라 시차입니다
원인은 read-after-write 일관성입니다. 생성 API는 성공했고 ID도 진짜지만, 조회 경로에는 아직 전파가 안 됐습니다. 몇 초 뒤에 읽으면 멀쩡히 나옵니다. 분산 시스템에서는 교과서적인 현상인데, 클라이언트 코드를 쓸 때는 다들 잊습니다 — 반환값에 ID가 있으니 당연히 조회도 되겠지, 하고요.
그런데 이 404 자체는 사고의 절반입니다. 나머지 절반이 더 나쁩니다.
진짜 사고: 크래시가 저장보다 먼저였습니다
원래 코드의 순서는 이랬습니다.
- 플레이리스트 생성 → ID 받음
- 그 ID로 기존 항목 조회 ← 여기서 크래시
- 영상 추가
- 상태 파일에 ID 저장 ← 여기까지 못 옴
2번에서 죽으면 ID는 메모리와 함께 증발합니다. 상태 파일에는 "이 시리즈의 플레이리스트 없음"이 그대로 남고, 다음 주 봇은 성실하게 같은 플레이리스트를 하나 더 만듭니다. 그다음 주에도요. 유튜브에는 삭제 전까지 같은 이름의 껍데기가 매주 하나씩 쌓입니다.
외부에 만들어진 리소스는 롤백이 없습니다. 중복을 지우러 갔다가 중복을 하나 더 만든 날에도 같은 계열의 교훈을 얻었는데, 이번 것이 더 구조적입니다: 돌이킬 수 없는 부수효과의 ID는 받는 즉시 저장해야 합니다. 배치 끝의 "마지막에 한 번 저장"은 그 사이의 모든 크래시에 인질로 잡혀 있습니다.
수정: 재시도가 아니라 조회 제거
404를 보고 반사적으로 떠오르는 수정은 재시도입니다. 잠깐 자고 다시 읽기, 지수 백오프, 최대 N회… 당신이라면 그렇게 고치겠습니까?
더 값싼 길이 있습니다. 방금 만든 플레이리스트는 비어 있습니다. 조회할 필요 자체가 없습니다. 답을 이미 아는 질문을 API에 물어보다가 죽은 겁니다.
그래서 수정은 두 줄 방향입니다:
- 생성 직후 즉시 상태 저장(크래시가 와도 ID는 살아남게)
- 방금 만든 플레이리스트면
fresh플래그로 기존 항목 조회를 통째로 건너뛰기(빈 집합으로 시작)
재시도 루프도, 백오프도 없습니다. 전파 지연과 경쟁할 이유가 사라졌으니까요.
자가진단 세 개:
- 외부 리소스를 만든 직후 그걸 다시 읽는 코드가 있습니까? 그 읽기는 정말 필요합니까?
- 생성된 ID가 저장되기 전에 죽을 수 있는 라인이 몇 개입니까?
- 그 코드가 크래시 후 재실행되면, 리소스를 또 만듭니까?
솔직한 부분
이 버그는 사실 두 번째 만남이었습니다. 같은 엔진을 쓰는 형제 채널에서 2주 전에 먼저 터졌고, 이번 채널에는 그 수정을 이식만 하면 됐습니다 — 같은 코드를 복제해 쓰면 버그도 채널 수만큼 복제됩니다. 그리고 첫 크래시 때 저장 못 하고 죽은 ID가 하나 있어서, 상태 파일에는 로그를 뒤져 사람이 손으로 복구해 넣은 항목이 하나 있습니다. 시스템은 고쳐졌지만, 과거 한 건은 결국 수작업이었습니다.
오늘 하나만 해보세요: 당신의 파이프라인에서 create 호출을 grep 해서, 반환된 ID가 몇 줄 뒤에 저장되는지 세어 보세요. 그 사이 줄 수가 곧 다음 중복 사고까지의 거리입니다.