무엇을 하고 싶었나
웹사이트의 문의 폼 API가 스프레드시트에 행을 추가하는 스크립트 서버를 바로 호출하고 있었어요. 이 스크립트 서버는 쓰는 동안 전체에 잠금(lock) 을 걸어서, 문의가 동시에 여러 건 들어오면 한 건만 들어가고 나머지는 "지금 바쁨" 에러로 실패했어요. 사용자는 에러를 보고 입력한 내용을 잃었어요.
미리 알면 좋은 용어 몇 가지예요.
- 큐(queue)와 워커(worker): 큐는 할 일을 줄 세워 두는 곳이고, 워커는 줄에서 하나씩 꺼내 실제 일을 하는 프로그램이에요. 식당의 주문서와 주방에 해당해요.
- ack(확인 응답): 워커가 큐에 "이 일은 끝났으니 지워도 돼요"라고 알리는 신호예요.
- 지터(jitter): 재시도 간격에 섞는 무작위 값이에요. 실패한 호출자들이 같은 순간에 돌아오지 않게 해 줘요.
무엇을 했나
흐름을 "API가 직접 호출"에서 "API는 큐에 넣고 바로 성공 응답, 워커가 천천히 적재"로 바꿨어요.
- API는 입력을 검증하고 문의마다 고유 번호(UUID)를 만들어 멱등 키로 붙여 큐에 넣은 뒤 바로 성공을 돌려줘요. 큐에 넣는 일 자체가 실패할 때만 같은 요청 안에서 직접 호출로 되돌아가요.
- 워커는 스크립트 서버를 한 번 호출해요. 실패가 "다시 해 볼 만한 것"(바쁨, 5xx, 시간 초과)이면 같은 배달 안에서 1~4초 무작위 간격으로 몇 번 더 시도해요.
- 그래도 안 되면 일부러 예외를 던져 큐가 점점 늘어나는 간격으로 다시 배달하게 해요. 최대 횟수를 넘기면 오류 추적 도구에 문의 번호와 함께 보고하고 그제야 ack해요.
- 스크립트 서버는 잠금 대기 → 문의 번호로 중복 검사 → 행 추가 → 즉시 응답만 하고, 알림은 주기 트리거가 따로 보내게 분리했어요.
무엇을 배웠나
큐를 둔다고 경합이 풀리지 않아요. 쓰고 있던 큐에는 "동시에 하나만 배달" 옵션이 없었어요. 그래서 열 건이 같은 순간에 워커로 배달됐고, 재배달도 같은 시각에 몰려 다시 잠금에 부딪혔어요. 순서를 정리하는 일은 워커 안에서 무작위 간격으로 재시도하는 쪽이 맡고, 큐의 긴 백오프는 상대가 오래 고장 난 경우에만 쓰게 두었어요. → 재시도, 지수 백오프, 지터
예외를 ack하면 조용히 사라져요. 사용자에게는 이미 "접수 완료"라고 답한 뒤라서, 워커가 예상 못 한 예외를 삼키고 정상 종료하면 문의가 사라져도 아무도 몰라요. 성공했을 때만 ack하고 멱등 키로 재배달의 중복을 견디게 했어요. → At-least-once 전달, ack 시점, 멱등 키
배포와 트리거가 분리된 런타임은 진입점이 보장해야 해요. 스크립트 서버의 시간 기반 트리거는 코드를 배포해도 저절로 생기지 않아요. 배포만 하고 트리거를 만들지 않으면 알림이 영원히 "대기" 상태로 남아요. 진입점이 자기 트리거를 확인하고 없으면 만들게 하자 수동 단계가 사라졌어요.
아직 확인하지 못한 것도 있어요. 큐 경로는 로컬에서 실행할 수 없어서 로컬 테스트가 폴백 경로로만 통과했고, 큐 경로는 운영 로그로만 확인했어요. 재시도 간격 옵션이 기대대로 적용되지 않은 원인도 추정에 그쳤어요.