GAS×AI 배치가 6분 제한으로 끊겨 요약본 380건을 이중 전송한 경험 — 체크포인트 재개와 LockService로 해결한 실패 사례 3가지
요약
GAS를 이용한 대규모 AI 요약 배치 처리 중 실행 시간 제한(6분)으로 인해 데이터 이중 전송 및 처리가 실패하는 사례와 해결책을 제시합니다. 근본 원인은 '전송 완료 여부'를 저장하지 않은 설계에 있었으며, 체크포인트(처리된 행 번호)와 자발적 일시 중지/재개 로직 구현을 통해 안정성을 확보할 수 있습니다.
핵심 포인트
- 배치 처리는 전송 완료 여부를 반드시 기록하는 체크포인팅이 필수입니다.
- 실행 시간 제한에 맞춰 스스로 중단하고 재개하는 원샷 트리거 방식이 안전합니다.
- 처리 범위는 행 번호 오름차순을 지키고, Script Properties를 진실의 근원으로 사용해야 합니다.
매일 아침, 스프레드시트에 쌓인 640건의 문의사항을 GAS(Google Apps Script) 스크립트를 이용해 LLM(Large Language Model)으로 요약 처리하고 있었습니다. 어느 날 아침, 실행 시간 제한인 6분 때문에 처리가 중간에 멈추었고, 급하게 수동으로 재실행한 결과, 이미 전송된 내용까지 포함하여 380건의 요약본이 Slack에 이중 게시되었고, API 할당량(quota)도 실질적으로 1.6배나 소진했습니다. 실패의 근본적인 원인은 '단 한 번의 실행으로 모든 건을 완료시키겠다'는 설계 방식에 있었습니다. 본문에서는 체크포인트(전송된 행 번호)와 자발적인 일시 중단을 통해, 6분 제한 내에서 여러 차례 나누어 처리하며 이중 전송 없이 완주할 수 있는 완전한 코드를 작성합니다.
실측 타임라인입니다. 건당 LLM 호출 약 0.6초 + 간격 조정 0.35초로 계산했을 때, 6분 동안 처리 가능한 건수는 약 380건이었습니다.
| 시간 | 발생한 일 | |
|---|---|---|
| 07:00:00 | 배치 시작. 작업량 640건 | |
| ... |
여기서 발생한 사고는 세 가지 계층으로 나눌 수 있습니다. ① 6분 제한에 의해 중단됨 (외부 요인). ② 재개 위치를 가지고 있지 않아, 끊기면 첫 번째 행부터 다시 시작함 (설계 결함). ③ '이미 전송된 행'을 모르기 때문에 이중 전송이 발생함 (데이터 손상).
②와 ③은 같은 근본 원인을 가집니다: '전송 완료 여부'를 어디에도 저장하지 않았던 것입니다.
처음에 작성했던 코드는 시트의 모든 행을 for문으로 순회하며 끝내는 50줄짜리 스크립트였습니다. 재개, 중단, 동시 실행과 같은 예외 처리가 전혀 되어 있지 않아, 6분이라는 상한선이 실제 작업 부하를 초과하는 아침에 모든 것이 무너졌습니다. 실패 사례를 정리하면 다음 표와 같습니다.
| 실패 유형 | 증상 | 올바른 대책 |
|---|---|---|
| 실행 시간 제한 | 6분 만에 강제 중단. 이후 행들이 미처리 상태로 방치됨 | 상한선 내부(5분 이내)에서 자발적으로 일시 중지하고, 다음 처리를 원샷 트리거로 재개함 |
| ... |
'끊기면 수동으로 다시 실행한다'는 운영 방식은 640건의 작업량이 한계치에 가까워지는 순간 무너집니다. 제한 값 자체가 변하지 않으므로, 설계를 제한 시간에 맞추는 것이 우선입니다.
재개 단위(처리 범위)마다 사고가 발생할 수 있는 시간 창이 달라집니다. 비교한 결과는 다음 표와 같습니다.
| 재개 단위 | 구현 방식 | 발생 가능한 사고 | 이중 전송이 발생하는 시간 창 |
|---|---|---|
| 전체 일괄 (실패 설계) | 6분 동안 최대치로 실행하고, 끊기면 첫 번째 행부터 다시 시작함 | 대량의 이중 전송 + 할당량 낭비 | 마지막 실행 시점부터 재개하는 전체 범위(6분 이상) |
| ... |
채택한 방식의 불변 조건은 간단합니다: **'처리는 반드시 행 번호 오름차순으로 진행되어야 한다'**와 **'전송 완료 ⟺ 행 번호 ≤ 체크포인트'**입니다. 이 두 가지를 지키는 한, 중단된 순간 아무리 되돌아가도 미전송된 첫 번째 행(checkpoint + 1)부터 재개하기만 하면 이중 전송은 단일 행의 범위(수백ms)로 줄어듭니다. status 열은 사람이 보기 위한 표시용으로 격하하고, '진실'은 Script Properties에 저장된 행 번호 하나로 통일합니다.
GAS의 실행 시간 상한은 6분입니다. 거의 한계까지 사용하면 마지막 행 쓰기 도중에 끊겨 데이터 정합성이 무너집니다. 그래서 시작 시각을 기록해 두고, 280초(여유 20초)를 초과하면 처리를 중단하고, 2분 뒤에 자신을 재실행하는 원샷 트리거를 생성합니다. 나머지가 소진될 때까지, 시작 → 일시 중지 → 계속이 연쇄적으로 발생합니다.
스프레드시트 Tasks 시트 (A~E열):
A1: 작업 ID B1: 원본 텍스트 C1: 상태 D1: 요약 결과 E1: 처리 완료
2행 이후: 작업 ID(비어있지 않으면 전송 대상), 원본 텍스트는 AI에 전달할 본문
참고로, 원샷 트리거는 실행할 때마다 소모품이므로 쌓여만 가지 않습니다. 잔여 업무가 0인 상태에서 끝나면 트리거는 생성되지 않고, 다음 날 아침의 dailyBatch만 실행됩니다. 주간 트리거 총 시간 상한(일 90분)에 도달하지 않도록, 연속 발화 간격은 2분으로 설정했습니다 (실측 결과 하루 총 실행 시간은 약 14분이었습니다).
수동 실행과 트리거가 동시에 실행되면, 체크포인트가 번갈아 읽고 쓰여서 시스템이 파탄 납니다. 맨 앞에서 tryLock을 사용하여 점유하지 못한 실행은 즉시 종료시킵니다. 10초만 기다리면 비어있다면 진행합니다 (연속 트리거 발화 간격으로는 거의 발생하지 않습니다).
// ============================================================
// AI 요약 배치: 체크포인트 재개 + LockService 이중 실행 가드
// 스크립트 속성 OPENAI_API_KEY를 setup()에서 설정하여 사용
...
시간대를 고정하는 매니페스트입니다. (동일 디렉터리의 appsscript.json을 덮어씁니다. atHour 지정 트리거는 시간대에 의존하므로 일본 시간을 명시합니다).
{
"timeZone": "Asia/Tokyo",
"exceptionLogging": "STACKDRIVER",
...
- 스프레드시트를 만들고 시트 이름을
Tasks로 지정한 후, 위의 'Tasks 시트' 레이아웃에 따라 A~E1 행에 헤더를 입력합니다. - 스프레드시트 메뉴에서 '확장 프로그램 → Apps Script'를 열고, main 부분의 코드를 그대로 붙여넣습니다. setup()을 한 번만 수동으로 실행합니다 (API 키 등록과 매일 07:00 트리거 생성을 한 번에 처리합니다. 키는 코드에 작성하지 않고, 실행 후 '프로젝트 설정 → 스크립트 속성'에서OPENAI_API_KEY값을 실제 키로 대체하는 것이 안전합니다) -testAi()를 실행하여 로그에 요약문이 반환되는 것을 확인합니다. - 시트의 2행 이후부터 태스크 ID와 원본 텍스트를 넣어두면, 07:00 트리거 또는 수동 실행인dailyBatch로 처리가 시작됩니다.
초기 수정 버전에서는 체크포인트(checkpoint) 대신 스프레드시트의 status 열만을 재개 키로 사용했습니다. 처리 전에 '처리 중', 전송 후에 '완료(done)'를 작성하는 방식입니다. 이 방식은 LLM 응답을 받은 직후부터 status 쓰기가 완료될 때까지 수백 밀리초의 틈을 행마다 대량으로 만듭니다. 저 역시 실제로 야간 트리거와 수동 테스트가 겹친 날, '완료' 작성 직전에 연결이 끊긴 행이 다음 날 이중 전송되는 경험을 했습니다. 수정된 부분은 '진실(truth)'을 status 열에서 단일한 행 번호로 대체하는 것이었습니다. 행 번호는 setProperty를 통해 한 번에 업데이트되며, 쓰기는 원자적(atomic)으로 처리되고, status 열은 단순한 화면 표시용으로 돌아갑니다.
| 항목 | 상한선 | 이 설계에서의 소비 |
|---|---|---|
| 1회 실행 시간 | 6분 | 280초로 자발적으로 중단 (여유 20초) |
| ... |
- 6분 제한은 '끊기기 전 280초에서 자발적으로 폴딩(folding, 중단)하고 원샷 트리거로 이어가기' 방식으로 설계 단계에 포함시켰습니다. 상한선 근처에서의 운영은 쓰기 도중에 끊겨 데이터의 일관성을 파괴할 수 있습니다.
- 전송된 진실은 하나로 한정합니다: 스크립트 속성(Script Properties)의 행 번호 (체크포인트). status 열은 표시용일 뿐입니다.
- 동시 실행은
LockService.tryLock으로 직렬화(serialize)합니다. 10초 안에 잠금을 얻지 못하면 즉시 종료해도 무방합니다. - 실패한 행은 체크포인트를 진행하지 않고 그 자리에서 멈춥니다. 후속 처리를 계속하는 것보다, 다음 날 아침 같은 행부터 자동으로 재개하는 것이 더 안전하며, 대량의 누락이나 이중 전송이 발생하지 않습니다. - 다음 조치: 현재 작동 중인 GAS 배치 작업을 하나 열어보고, '끊겼을 때의 재개 위치'와 '전송된 저장소'가 코드에 작성되어 있는지 확인하십시오. 둘 다 없다면, 본 기사의 체크포인트 방식을 한 번에 적용하는 것이 가장 빠릅니다.
참고 도서: Google Apps Script × AI 실전 입문 — 스프레드시트로 구동하는 AI 워크플로우 (Kindle・Kindle Unlimited 무제한 읽기 대상) (하야마 유우키 저)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기