텔레그램 한 줄로 블로그 작업을 넘길 때 중복 실행을 막는 구조

답부터 말하면 텔레그램 메시지 ID를 요청 파일 이름과 처리 영수증의 공통 키로 써야 한다.
같은 메시지가 다시 들어와도 새 파일로 덮어쓰지 않고, 상태 파일이 있으면 에이전트에 다시 넣지 않는 구조다.

이 결론은 2026년 9월 28일 로컬 테스트 기준이다.
테스트 41개가 통과했고 실패는 0개였다.
다만 실제 텔레그램 사진 왕복과 장애 복구 시간은 아직 측정하지 않았다.

텔레그램 메시지를 한 번만 실행하는 파일 영수증 구조




왜 명령 한 줄이 두 번 실행될 수 있나?

텔레그램에서 /blog 명령을 한 번 보냈다고 실행도 한 번이라고 볼 수 있을까.
수신 플러그인, 파일 감시, 에이전트 세션, 답장 전송 사이에는 각각 재시작과 재호출이 들어올 수 있다.

문제는 요청 내용이 아니라 경계다.
같은 업데이트를 플러그인이 다시 받거나, 파일 감시 이벤트가 반복되거나, 답장 전송 중 프로세스가 멈출 수 있다.
그래서 “이미 본 요청인가”를 각 단계가 같은 기준으로 판단해야 한다.

이 브리지는 메시지 본문 대신 텔레그램 메시지 ID를 기준으로 삼는다.
내용이 같은 두 요청은 서로 다른 작업일 수 있지만, ID가 같은 요청은 같은 전달 건이기 때문이다.




요청 파일은 어떻게 한 번만 만들어지나?

수신 단계는 먼저 임시 파일에 요청 전체를 쓴다.
파일을 동기화하고 닫은 뒤, requests/<메시지ID>.json이라는 최종 이름에 배타적으로 연결한다.

최종 파일이 이미 있으면 덮어쓰지 않는다.
기존 파일 자체가 중복 수신 영수증이 된다.
소비자는 완성된 이름만 보므로 절반만 써진 JSON을 집어 들지 않는다.

  1. 임시 파일에 요청 전체를 기록한다.
  2. 파일 동기화와 닫기를 끝낸다.
  3. 메시지 ID 이름으로 배타적 링크를 만든다.
  4. 같은 이름이 있으면 기존 요청을 유지한다.
  5. 요청 파일은 처리 뒤에도 삭제하지 않는다.

2026년 9월 28일 테스트에는 같은 명령을 8개 동시에 넣어도 최종 요청 파일이 하나만 남는 검사가 있다.
파일 감시자가 처음 본 시점에 JSON 전체를 읽을 수 있는지도 별도로 확인한다.




소비자는 무엇으로 재실행을 막나?

요청 파일 하나만으로는 부족하다.
에이전트에 전달한 뒤 프로세스가 다시 요청 폴더를 훑을 수 있기 때문이다.

소비자는 같은 ID의 state/<메시지ID>.json이 있으면 그 요청을 건너뛴다.
전달 직전에는 submitted, 텔레그램 답장 전송 직전에는 sending, 전송이 끝나면 sent를 기록한다.
전송 오류는 failed, 잘못된 요청은 rejected로 남는다.

여기에 소비자 잠금이 하나 더 있다.
한 세션을 두 프로세스가 동시에 소비하지 못하도록 consumer.lock을 배타적으로 만든다.
세션이 바쁘면 요청을 보관하고, 작업이 끝난 뒤 다음 요청 하나를 꺼낸다.

요청 파일과 상태 영수증으로 중복 실행을 막는 흐름
중복 방지 장치별 역할
장치막는 문제남는 한계
메시지 ID 요청 파일같은 수신 이벤트의 덮어쓰기·중복 등록서로 다른 ID의 같은 내용은 별도 요청이다
상태 영수증재스캔 뒤 에이전트 재주입실패 상태를 자동 재시도하지 않는다
소비자 잠금두 프로세스의 동시 소비살아 있는 다른 프로세스가 잠금을 쥐면 새 소비자는 붙지 못한다. 죽은 프로세스의 잠금만 자동 인계한다
답장 연결 영수증일반 대화와 블로그 후속 답장 혼입등록되지 않은 답장은 기존 봇 흐름으로 간다




왜 failed를 자동 재시도하지 않나?

전송 실패를 보면 바로 다시 보내고 싶어진다.
그런데 sending 중단은 가장 애매한 구간이다.
텔레그램이 답장을 받았지만 로컬 영수증을 쓰기 전에 프로세스가 멈췄을 수도 있다.

이때 자동 재시도하면 같은 답장이 두 번 갈 수 있다.
그래서 이 구현은 submitted, sending, failed를 임의로 다시 보내지 않는다.
실패 테스트도 전송 시도가 한 번뿐이고 상태가 failed로 남는지 확인한다.

택배 송장과 비슷하다.
송장 번호가 같으면 새 상자를 만들지 않는다는 점은 닮았다.
다만 파일 영수증은 외부 전송 완료까지 원자적으로 묶지 못하므로, 배송 완료 여부는 따로 확인해야 한다.

이 선택은 완전한 “정확히 한 번” 보장이 아니다.
중복 실행을 피하는 대신 일부 실패를 자동 복구하지 않는 보수적 설계다.
외부 부작용을 어디서 승인할지는 Discord 승인과 launchd로 통제권을 설계한 글의 주제이고, 이번 구조는 승인 전후가 아니라 전달 계층의 중복 방지만 다룬다.




실제로 어디까지 확인했나?

2026년 9월 11일 기록에는 초기 27개 테스트 통과와 텍스트 실제 왕복 1회가 남아 있다.
별도의 블로그 알림 연결 시험도 수신 확인이 기록돼 있다.
이 알림 시험은 에이전트 답장 라우팅 검증과는 다른 증거다.

2026년 9월 28일 같은 로컬 테스트 명령을 다시 실행하니 범위가 41개로 늘어 있었다.
41개 모두 통과했고, 전체 행 커버리지는 94.19%였다.
이미지 모듈 행 커버리지는 100%였지만, 이 숫자는 실제 사진 왕복 성공률이 아니다.

테스트가 확인한 범위는 동시 중복 등록, 완성 파일 공개, 바쁜 세션 대기, 반복 스캔, 다른 세션 차단, 전송 실패 후 자동 재전송 금지, 텍스트·이미지 모의 왕복이다.
실제 텔레그램 사진 왕복, 처리량, 재시도 복구 시간은 측정하지 않았다.




어떤 조건에서 이 결론이 바뀌나?

작업이 조회나 초안 생성처럼 다시 실행해도 부작용이 없다면, 실패를 멈춰 세우는 것보다 자동 재시도가 나을 수 있다.
반대로 발행·결제·삭제처럼 중복 비용이 큰 작업이라면 현재처럼 영수증을 남기고 사람이 실패 상태를 확인하는 편이 낫다.

작업 성격에 따른 실패 처리 선택
작업 성격예실패 시 선택이유
다시 실행해도 부작용이 없다조회, 초안 생성자동 재시도중복 실행 비용보다 멈춤 비용이 크다
중복 비용이 크다발행, 결제, 삭제영수증을 남기고 사람이 확인두 번 실행되는 손해가 복구 지연보다 크다

여러 장비가 같은 요청을 소비한다면 로컬 파일 잠금만으로는 부족하다.
그때는 공유 데이터베이스의 고유 키와 트랜잭션, 또는 외부 작업의 멱등성 키가 필요하다.




결론 · 먼저 영수증을 남긴다

텔레그램 명령의 안전성은 빠르게 전달하는 것보다 같은 요청을 다시 실행하지 않는 기준에서 시작한다.
자동 재시도 여부는 중복 실행의 손해와 멈춤 비용 중 더 큰 쪽을 기준으로 정하면 된다.
지금은 메시지 ID 요청 파일, 상태 영수증, 단일 소비자 잠금이 각각 어디서 중복을 막는지 먼저 확인하면 된다.

댓글

이 블로그의 인기 게시물

AI 코딩 스킬, 많을수록 좋을까 — 자산이 부채로 바뀌는 손익분기

Opus 5는 더 잘 쓰고, 더 AI답게 쓴다 — 세 모델 첫날 실측

M4 Max 128GB 로컬 LLM 4종 속도 비교 — 실측 기록과 한계