명품몰 상품 이미지의 썸네일이 제대로 생성됐는지 확인해야 했다. GET /thumb/{id}?w=300 으로 요청을 보내 200 이 오면 있는 것으로 보고 전부 있다고 보고했다.
Table of contents
Open Table of contents
증상 — 200 이 온 확인 요청
며칠 뒤에 일부 상품 목록에서 이미지가 늦게 뜬다는 제보가 왔고 확인했던 것 중에도 있었다. 오브젝트 스토리지의 thumbnails 버킷을 직접 조회하니 그 썸네일이 없었다.
없는데 확인할 때는 200 이 왔다는 뜻이다. 그러면 응답을 만든 것이 무엇인지를 봐야 했다.
원인 — 관측이 대상을 바꿨다
ThumbnailController 를 보니 요청한 썸네일이 없으면 그 자리에서 convert 로 만들어 버킷에 저장한 뒤에 돌려주고 있었다. 미리 다 만들지 않고 요청이 올 때 만드는 지연 생성이었다.
내가 보낸 확인 요청이 변환을 촉발했으므로 200 은 원래 있었다는 증거가 아니라 그 요청이 만들었다는 증거였다. 관측이 대상을 바꾸면 그 관측은 증거가 되지 못한다.
조치 — 저장소를 직접 보는 확인
두 가지 방법이 있었다. ThumbnailController 에 생성 없이 존재만 답하는 ?check=true 를 붙이는 것과, 확인 스크립트가 doesObjectExist() 로 버킷을 직접 조회하는 것이다. 뒤쪽을 골랐다. 이유는 확인 대상과 확인 수단을 같은 계층에 두면 그 경로가 어떤 부수효과를 갖는지 모른 채로 판정하게 되기 때문이다. 대신 확인 스크립트가 스토리지 접근 키를 따로 가져야 하는 것을 감수했다.
버킷 조회는 읽기만 하므로 없으면 없다는 결과가 그대로 나온다. 확인 대상과 확인 수단을 같은 계층에 두지 않는 것이 이 건의 교훈이었다.
주의 — 부수효과가 있는 조회들
이후로 조회 경로를 쓸 때 이것을 부르면 무슨 일이 일어나는지를 함께 묻게 됐다. RFC 7231 은 GET 을 안전한 메서드로 정의하면서도 구현이 부수효과를 갖는 것 자체를 막지는 않는다고 적고 있다. 없으면 만드는 것 말고도 호출 수를 올리거나 캐시를 채우거나 접근 기록을 남기거나 비용이 붙는 조회가 있었다.
읽기니까 안전하다는 문장이 항상 참은 아니다. 통계가 오염되거나 다음 사람에게 옛 데이터가 나가는 것도 확인 행위가 만든 결과다.
없다의 세 가지 이유
같은 조사에서 어떤 원본은 가로 300 미만이라 애초에 썸네일 대상이 아니라는 것도 확인했다. 그러면 썸네일이 없다는 관측이 실패와 미완료와 대상 아님 셋 중 하나가 된다.
셋째를 모르면 첫째로 오해하고 계속 재시도하게 된다. 그래서 원본 존재와 변환 대상 여부와 썸네일 존재와 thumb_jobs 이력을 단계로 나눠 어느 단계에서 갈리는지를 원인으로 삼았다.
정리
- 관측이 대상을 바꾸면 그 관측은 증거가 못 된다
- 확인 요청이 생성을 촉발하면
200이 존재의 증거가 아니다 - 존재 확인은
doesObjectExist()로 저장소를 직접 조회한다 - 확인 대상과 확인 수단을 같은 계층에 두지 않는다
- 조회 경로를 쓸 때 부르면 무슨 일이 일어나는지 묻는다
GET이라고 안전한 것이 아니라 생성과 통계와 캐시가 함께 움직인다- 없다는 관측이 실패와 미완료와 대상 아님으로 갈린다
- 확인을 단계로 나누면 어디서 갈리는지가 곧 원인이 된다