지인들과 소소하게 운영하는 블로그에 자동 포스팅 배치를 돌려두고 편하게 쉬던 날이었습니다. 갑자기 메일함에 배치 에러 알림이 가득 쌓이기 시작했습니다. 확인해 보니 외부 이미지 생성 API인 Pollinations 쪽에서 HTTP 530 에러를 뿜어내고 있었습니다. 이미지 생성 단계에서 예외가 발생하면서, 무려 1,949자나 잘 써놓았던 양질의 본문까지 통째로 폐기되고 처음부터 다시 생성되는 참사가 발생했습니다. 외부 API 하나가 흔들렸다고 전체 프로세스가 무너지는 비효율적인 구조였던 것입니다.
전부 성공 아니면 전부 실패? 이분법적 설계의 한계
처음에는 단순히 네트워크 일시 오류겠거니 하고 대수롭지 않게 넘기려 했습니다. 하지만 외부 서비스의 불안정성은 내가 통제할 수 있는 영역이 아니었습니다. 하나의 트랜잭션 안에서 텍스트 생성과 이미지 생성을 모두 완벽히 끝내려다 보니, 정작 가장 리소스가 많이 드는 본문 생성 노력이 매번 물거품이 되곤 했습니다. 부분 성공 처리가 전혀 고려되지 않은 이분법적 배치 실패 복구 설계의 뼈아픈 한계였습니다.
이미 생성된 훌륭한 텍스트 콘텐츠를 보존하면서, 실패한 이미지 영역만 나중에 메워 넣을 수 있는 영리한 우회로가 절실했습니다. 그래서 이미지 단계에서 에러가 나더라도 전체 프로세스를 중단시키지 않고, 실패한 자리를 임시 마커로 대체해 두는 아이디어를 떠올렸습니다.
임시 마커와 GAS Script Properties를 활용한 데이터 보존
문제를 해결하기 위해 먼저 이미지 생성 실패 시 예외를 던지는 대신, 본문 내 실패한 이미지 자리를 <!--IMAGE_N--> 형태의 주석 마커로 대체하도록 로직을 수정했습니다. 이렇게 하면 본문 생성 자체는 안전하게 완료할 수 있습니다. 그 다음 단계로, 발행을 잠시 보류하고 생성된 본문 데이터와 메타 정보, 이미지 생성용 프롬프트를 GAS Script Properties에 안전하게 저장했습니다.
동시에 데이터베이스 역할을 하던 노션의 상태값을 '이미지누락'으로 업데이트했습니다. 구글 앱스 스크립트(GAS)의 제한적인 환경에서도 GAS Script Properties 덕분에 무거운 데이터베이스 인프라 없이도 필요한 상태와 텍스트를 고스란히 보존할 수 있었습니다.
기존 발행 경로를 재사용하는 스마트한 이미지 생성 재시도
이제 보관된 임시 데이터를 가지고 복구할 차례였습니다. 다음 배치 주기나 특정 시점에 작동할 completePendingImages 함수를 새로 설계했습니다. 이 함수는 대기 상태에 있는 누락분을 찾아내어 오직 실패했던 이미지 생성 재시도 작업만 수행합니다. 이미지가 성공적으로 생성되면 기존 본문에 심어두었던 마커를 실제 이미지 주소로 깔끔하게 교체합니다.
여기서 핵심은 복구 경로를 완전히 새로 만드는 대신, 기존의 정상적인 발행 함수인 publishToBlogger를 그대로 재사용해 발행을 완료한다는 점이었습니다. 덕분에 중복 코드 없이 깔끔한 복구 파이프라인이 완성되었습니다. 다만 무한 루프에 빠지는 것을 막기 위해 재시도 횟수가 5회를 초과하면 자동으로 펜딩 상태를 해제하고 실패 처리하도록 안전장치도 함께 마련해 두었습니다.
실패를 대하는 태도: 복잡성을 늘리지 않는 우아한 복구
이번 작업을 통해 배치 실패 복구 설계에서 중요한 교훈을 얻었습니다. 시스템의 안정성을 높이기 위해 무작정 복구만을 위한 새로운 상태나 복잡한 경로를 추가할 필요가 없다는 점입니다. 오히려 기존에 잘 작동하는 정상 경로를 어떻게 유연하게 재활용할지 고민하는 것이 훨씬 우아한 해결책이 되곤 합니다.
외부 서비스의 API는 언제든 불안정해질 수 있습니다. 전부 아니면 전무라는 이분법적 생각을 버리고, 부분 성공을 인정하며 다음 사이클로 이어붙이는 설계가 얼마나 든든한지 온몸으로 깨달은 유익한 경험이었습니다.