첫 번째 삽질: 1시간 간격 설정과 원인 불명의 스킵 현상
처음에는 더 욕심을 내서 1시간 간격으로 트리거를 촘촘하게 배치해 보았다. 그러나 실행 결과를 확인해 보니 이상하게도 발행된 글의 개수가 기대했던 것의 절반밖에 되지 않았다. 무엇이 문제인지 로그를 뜯어보니, 내가 runMigration 함수 내부에 걸어둔 안전장치가 발목을 잡고 있었다. 마지막 글 발행 이후 1시간이 지나지 않았다면 중복 발행이나 스팸 방지를 위해 실행을 건너뛰도록 만든 '마지막 발행 후 1시간 미만이면 스킵' 가드 로직이 문제였다.
분명히 정각 단위로 1시간 간격 트리거를 설정해 두었는데 왜 가드 로직에 걸려 스킵이 발생한 것일까? 원인은 구글 앱스 스크립트(GAS)의 시간기반 트리거 작동 방식에 있었다. GAS에서 제공하는 시간기반 트리거 API는 특정 시간(atHour)만 지정할 수 있을 뿐, 구체적인 분(minute) 단위 제어가 불가능하다. 즉, 실행 분은 구글 서버의 부하 분산을 위해 시스템이 무작위로 결정하는 구조다.
이로 인해 2시 트리거는 2시 58분에 실행되고, 바로 다음인 3시 트리거는 3시 2분에 실행되는 기현상이 발생할 수 있었다. 두 실행 사이의 실제 물리적 시간 간격이 고작 4분밖에 되지 않았던 것이다. 결국 내 스크립트에 들어 있던 1시간 가드 조건에 걸려 3시 트리거는 아무 작업도 하지 않고 스킵 처리되는 상황이 반복되었다. 시간기반 트리거의 이 무작위 분 실행 특성을 간과한 결과였다.
해결책: 최소 간격 가드를 고려한 GAS 트리거 간격 설계
문제를 해결하기 위해서는 내부의 최소 간격 가드 로직과 외부의 GAS 트리거 간격 설계를 입체적으로 조율해야 했다. 무작위 분 제어가 안 된다면, 트리거 간격을 충분히 벌려서 어떤 경우에도 가드 시간인 60분을 침범하지 않도록 안전 마진을 확보하는 방법이 최선이었다. 그래서 트리거를 무리하게 1시간 간격으로 배치하는 대신, 2시간 간격으로 다시 조정했다.
트리거 간격을 2시간으로 설계하면 수학적으로 완벽한 안전 영역이 확보된다. 예를 들어 이전 트리거가 가장 늦은 시간인 0시 59분에 실행되고, 다음 트리거가 가장 빠른 시간인 2시 00분에 실행되더라도 최소 실행 간격은 61분이 보장된다. 이는 내 가드 조건인 60분을 아슬아슬하게 넘어서는 완벽한 안전거리였다.
이렇게 GAS 트리거 간격 설계를 바꾼 후, 스크립트는 단 한 번의 가드 충돌도 없이 안정적으로 돌아가기 시작했다. 하루에 설정한 12건의 트리거가 누수 없이 100% 제 역할을 다하며 글을 성공적으로 발행했다. 덕분에 마이그레이션 완료 기간도 계획했던 대로 15일로 크게 줄일 수 있었으며, 자동화 스크립트의 실행 신뢰성은 최고 수준으로 올라갔다.
추가적인 수확: 이미지 생성 쿼터 분리와 진짜 병목 찾기
이 과정에서 마이그레이션 효율을 극대화하기 위해 몇 가지 추가 조치를 함께 취했다. 먼저 마이그레이션에 필요한 이미지 생성에 무료 API인 Pollinations를 적극 활용했다. 기존의 Gemini나 구글 자체 이미지 쿼터와 완벽하게 분리해 둔 덕분에, 발행량을 대폭 늘려도 내가 운영 중인 다른 자동화 블로그의 AI 할당량을 잠식하지 않는 독립적인 구조를 완성할 수 있었다.
또한 작업을 이어가며 깨달은 진짜 병목은 따로 있었다. 바로 구글 계정 단위로 공유되는 GAS 트리거의 하루 최대 90분 런타임 제약이었다. 간혹 마이그레이션 도중 429 에러(Too Many Requests) 같은 일시적 제한을 만나면 스크립트가 대기(sleep)하며 재시도를 수행하는데, 이 대기 시간마저도 90분 런타임 한도에 그대로 누적되어 소모된다.
한 번 실행할 때 적용되는 6분 실행제한뿐만 아니라, 하루에 쓸 수 있는 전체 누적 시간 자체가 매우 귀하다는 사실을 뼈저리게 실감했다. 결국 단순한 실행 빈도 증가보다 대기 시간을 최소화하는 효율적인 네트워크 요청 처리가 안정적인 자동화의 핵심이라는 인사이트를 얻었다. 이번 삽질을 통해 분 단위 제어가 안 되는 구글 트리거의 한계를 극복하고 나만의 견고한 마이그레이션 시스템을 구축하게 되어 매우 뿌듯하다.