사람 없이 도는 배치 발행 — 한 편이 깨져도 나머지는 가게 만들기

앞 글에서 글 한 편을 제대로 올리는 데 필요한 것들을 적었다. 이번은 그걸 여러 편 한 번에, 사람이 안 보고 있을 때 돌리는 얘기다.
한 편씩 올릴 때는 실패해도 별 문제가 없었다. 내가 화면을 보고 있으니 어디서 멈췄는지 바로 알고, 고쳐서 다시 누르면 됐다. 여러 편을 묶어 돌리기 시작하면서 그 전제가 사라졌다. 돌려놓고 자리를 비우면, 돌아왔을 때 남은 건 결과 파일 하나뿐이다.
한 편 때문에 전부 날아갔다
처음 만든 배치는 목록을 받아 순서대로 발행하는 단순한 반복문이었다. 문제는 그 안에서 예외가 하나 올라오면 반복문 자체가 끝난다는 것이었다. 세 번째 글의 원고가 깨져 있으면 네 번째부터 열 번째까지는 시도도 안 해보고 배치가 죽었다.
그래서 발행 로직을 항목 하나를 처리하는 함수로 통째로 빼내고, 그 호출만 예외로 감쌌다. 예외가 나면 그 항목의 실패 행을 만들고 다음으로 넘어간다.
for item in items:
try:
results.append(run_item(item))
except Exception as e:
results.append(fail(item, type(e).__name__))
write_out(results)
여기서 예외 종류를 좁히지 않고 전부 잡게 둔 건 의도였다. 목적이 “어떤 예외든 다음 항목은 계속”이라서, 잡을 예외를 특정하는 순간 예상 못 한 종류가 격리를 뚫고 나간다. 대신 실패 사유에 예외 이름을 넣어 진단에 쓸 거리는 남겼다.
로그도 바꿨다. 실패 사유가 여러 줄짜리 역추적으로 나오면 로그 한 줄이 항목 하나라는 규칙이 깨진다. 여러 줄은 공백으로 접어서 한 줄로 기록하게 했다.
실패한 항목이 어느 글인지 몰랐다
격리를 넣고 나서 다른 게 걸렸다. 실패 행은 남는데, 그게 어느 글인지 알 수 없는 경우가 있었다.
식별자를 원고 앞머리의 메타데이터에서 읽고 있었기 때문이다. 그 메타데이터를 파싱하다 실패해서 항목이 죽으면 식별자도 같이 못 읽는다. 결과 파일에는 이름 없는 실패 한 줄만 남는다.
대기 폴더의 파일 이름에 식별자가 들어가 있어서, 메타데이터를 못 읽은 항목은 파일명에서 식별자를 되살려 로그와 결과에 적게 했다. 실패해도 어느 글이 실패한 건지는 알아야 다시 돌릴 수 있다.
실패한 항목의 순번도 그대로 유지하게 했다. 뒤에 붙은 정리 단계가 순번 정렬에 의존해서, 실패한 자리를 그냥 빼버리면 뒤가 어긋난다.
깨진 입력을 어디까지 살릴까
메타데이터가 깨지는 패턴은 대체로 두 가지였다. 하나는 제목이 따옴표로 시작하고 뒤에 말이 더 붙은 경우, 하나는 물음표가 든 주소가 목록 안에 그냥 들어간 경우다. 둘 다 작성 단계가 실제로 내는 지뢰였다.
고민한 건 어디까지 자동으로 고칠까였다. 결론은 값을 다시 인용해 한 번 더 파싱해보는 선까지였다. 그걸로 살아나면 발행하고, 안 살아나면 그 항목만 실패로 둔다.
더 손대지 않은 이유가 있다. 목록이 문자열로 뭉개지는 식으로 뜻이 바뀌면, 발행은 성공으로 끝나고 내용만 조용히 잘못 나간다. 실패는 눈에 보이지만 이건 안 보인다. 한 편 빠지는 게 낫다고 보고, 복잡한 구조가 섞인 값은 아예 건드리지 않게 했다. 타입이 보존되는지는 테스트로 못박았다.
성공한 편까지 미확인으로 보고됐다
제일 뒤늦게 발견한 게 결과 파일이었다. 결과를 모아 마지막에 한 번 쓰게 만들어뒀는데, 그게 중간에 죽으면 아무것도 안 남는다는 뜻이었다.
실제로 당했다. 다섯 편을 돌렸고 네 편이 올라갔는데, 명령 실행 시간 상한을 넘겨 중간에 끊겼다. 결과 파일은 0바이트도 생기지 않았고, 호출한 쪽은 다섯 편 전부를 미확인으로 봤다. 올라간 네 편이 기록에 없어서 다시 올릴 위험까지 생겼다.
발행 로그는 이미 항목마다 쓰고 있었다. 중간에 죽어도 보이게 하려고 그렇게 만들었는데, 결과 파일만 전부 아니면 전무였던 것이다. 계약을 맞춰서 항목이 끝날 때마다 지금까지의 결과 전체를 덮어쓰게 했다.
이 쓰기가 실패하면 삼킨다. 결과 기록이 발행 자체를 막아서면 순서가 뒤집힌다.
시험 실행이 기록을 더럽혔다
시험 실행 모드를 만들어뒀다. 실제로 올리지 않고 파싱과 순서만 확인하는 모드다.
그런데 발행 로그를 쓰는 부분을 거기서 막아두지 않아서, 시험 실행이 운영 로그에 가짜 성공 줄을 남긴 적이 있다. 나중에 로그를 보고 이미 올라간 글이라고 착각할 수 있는 흔적이다. 로그 기록을 시험 실행에서는 아예 건너뛰게 막고, 시험 실행은 아무것도 건드리지 않는다는 규칙으로 고정했다.
쌓이는 입력 파일 치우기
실행할 때마다 입력 목록 파일이 한 벌씩 생겼고, 치우는 코드가 없어서 그냥 쌓였다.
그냥 다 지워도 됐다. 발행이 끝난 뒤 그 파일을 다시 읽는 코드는 없다. 그래도 최신 몇 벌은 남기기로 했다. 발행 로그에는 결과만 있고 입력이 없어서, 사고가 났을 때 무엇을 넣었길래 깨졌는지 확인할 유일한 증거가 입력 파일이었다.
그래서 오래된 실행분만 지우는 정리를 넣었다. 파일 이름이 날짜와 시각으로 시작해서 사전순 정렬이 곧 시간순이라, 날짜를 파싱할 필요가 없었다.
조심한 데가 두 곳이다. 지금 돌고 있는 실행분은 이름 비교로 무조건 제외했다. 정리하다 자기 입력을 지우면 방금 말한 증거가 제일 먼저 사라진다. 그리고 지우기가 실패하면 삼키게 했다. 파일이 안 치워진 것보다 발행이 멈추는 쪽이 손해가 크다.
보관 수는 처음에 다섯 벌로 잡았는데 부족했다. 입력 파일은 실행 한 번이 아니라 사이트 하나를 돌 때마다 생기고, 배치는 사이트를 순차로 돈다. 그래서 한 라운드만 돌아도 보관분이 거의 다 밀려나갔다. 사이트 수에 비례해 올려서 며칠치 라운드가 남게 했다.
정리하면
무인으로 돌리는 것과 보면서 돌리는 것은 다른 일이었다.
보면서 돌릴 때는 실패가 그냥 실패다. 눈앞에서 멈추고 내가 고친다. 사람이 없으면 실패가 두 번 일을 만든다. 한 번은 실패 자체로, 또 한 번은 실패를 어떻게 기록했느냐로. 기록이 틀리면 멀쩡히 올라간 글을 다시 올리거나, 빠진 글을 올라간 줄 알고 넘긴다.
이번에 고친 것들을 한 줄로 줄이면 전부 같은 말이다. 실패를 작게 가두고, 결과는 미루지 말고 그때그때 남긴다. 한 항목의 예외는 그 항목에서 끝내고, 결과 파일은 끝까지 모았다가 쓰지 않고, 식별자는 메타데이터가 깨져도 파일명에서 되살리고, 지우는 작업은 지금 쓰는 것부터 제외한다.
반대로 하지 않은 것도 비슷한 기준이었다. 깨진 입력을 추측으로 고치지 않았다. 조용히 틀린 결과는 요란한 실패보다 비싸다.
여기까지가 도구를 만든 이야기다. 열한 편에 걸쳐 설계부터 토큰, 브라우저, 이미지, 장부, 무인 실행까지 적었다. 이제 도구는 돌아가니 다음부터는 그걸로 사이트를 굴리면서 생기는 일을 쓴다. 글이 색인에 안 잡히는 문제, 쌓이는 글 수와 실제로 들어오는 사람 수의 간격, 그리고 애드센스 쪽 진행 같은 것들이다. 만드는 얘기는 끝났고 운영하는 얘기가 시작된다.


