버튼은 계속 돌고, 탭에는 오류가 뜨고, 십 분 동안 채운 양식은 사라졌다. 알 수 없는 것은 연결이 끊기기 전에 쓰기가 도달했는지다. 이 미지를 실패로 단정하는 것이 중복 댓글, 이중 주문, 반복되는 지원 요청을 만든다.

정해진 순서로 복구한다. 텍스트를 남기고, 결과를 찾고, 그다음에야 두 번째 제출이 허용되는지 판단한다.

텍스트와 타임스탬프부터 시작한다

다른 무엇을 건드리기 전에 페이지에서 내용을 복사한다. 브라우저가 아직 입력란에 들고 있는 임시 저장도 포함한다. 시각, 머물던 페이지, 한 동작을 적어 둔다. 이 세 가지가 나머지 조사의 근거가 된다. 탭이 아직 열려 있다면 네트워크 항목을 읽을 때까지 열어 둔다. 텍스트를 저장하는 데는 비용이 없지만 잃는 데는 작업 전체가 든다.

그다음 실제로 관찰할 수 있는 두 가지 장애 형태를 나눈다. 요청 도중 끊긴 연결과 오류로 응답한 서버는 네트워크 패널에서 다르게 보이며, 앱의 판단에 대해 무언가를 말해 주는 것은 후자뿐이다.

앱 자체의 보기에서 결과를 찾는다

결과를 보여 줄 읽기 보기를 새로 고친다. 댓글 목록, 주문 내역, 티켓 대기열, 편집 중이던 레코드 등이다. 가장 최근 항목이 아니라 계정, 목적지, 시각, 내용으로 대조한다. 나보다 뒤에 다른 사람이 썼을 수 있기 때문이다. 백그라운드 작업이라면 연결 시험용으로 새 작업을 만들지 말고 기존 참조와 그 상태를 조회한다.

결과가 있으면 끝이고, 오류는 쓰기가 아니라 응답 경로의 것이다. 결과가 없으면 앱이 아직 확정하지 않았을 가능성을 남긴다. 대기 중인 작업, 지연된 읽기 보기, 뒤처지는 복제본은 성공한 쓰기를 한동안 감출 수 있다.

결론을 내리기 전에 정해 둔 두 번째 확인을 하고, 두 확인의 시각을 기록한다. 이십 초 간격의 두 확인은 십 분 간격의 두 확인보다 훨씬 약한 증거다.

두 번째 제출이 안전한지 판단한다

답은 작업의 종류에 달려 있고, 인내심에 달려 있지 않다. HTTP는 같은 의도한 효과로 반복할 수 있는 메서드와 그렇지 않은 메서드를 구분하고, 의미론이 허용한다고 알려지지 않은 한 후자의 자동 재시도를 권하지 않는다. 댓글 작성, 주문, 결제 전송은 대개 후자에 속한다. 그래서 성공한 듯 보이는 두 번째 클릭은 첫 번째에 대해 아무것도 말해 주지 않는다.

클라이언트를 유지보수한다면 API가 문서화한 재시도나 멱등성 메커니즘을 사용하고, 시도마다 새 키를 만드는 대신 같은 논리 작업에 연결한다. 추적용 요청 식별자는 서버가 그렇게 취급하지 않는 한 중복 억제 키가 아니다.

curl -i -X POST https://api.example.net/v1/orders \
  -H "Idempotency-Key: 6f1c2a94-3b7e-4a10-9c22-5d0e8f7b1a63" \
  -H "Content-Type: application/json" \
  --data @order.json

키는 서비스가 그것을 작업에 대응해 저장하고 반복 시 원래 결과를 돌려줄 때만 도움이 된다. 운영에 의존하기 전에 스테이징 엔드포인트에서 그 동작을 시험한다.

다시 누르기 전에 증거를 남긴다

오류 스크린샷이 아니라 네트워크 항목을 남긴다. 요청 메서드, URL, 응답 또는 타임아웃, 발생 시각이 관리자가 앱 기록과 대조할 수 있는 정보다. 항목을 명령으로 내보내거나 복사하려면 그 뒤에 복사본에서 쿠키, 인증 헤더, 사적인 내용을 모두 제거해야 한다. 자신의 세션은 그대로 두고, 가로챈 자격 증명이 담긴 요청을 지름길로 재전송하지 않는다.

서비스 소유자에게 구체적인 질문 하나를 한다. 이 내용으로 이 시각의 작업이 성공했는지, 참조가 있는지. 그 질문에는 답이 있지만 "사이트가 망가졌는지"에는 답이 없다.

이 연습의 목적은 동작이 정확히 한 번 일어났음을 확인하는 것이다. 닫아 버릴 수 있는 오류 메시지는 믿을 수 있는 기록과 같지 않으며, 다른 무엇을 보내기 전에 필요한 것은 후자다.

관련 글