백 개 가까운 항목을 순회하며 처리하는 배치가 중간에 멈췄다. 한 항목에서 난 예외가 루프 밖으로 나가면서 나머지가 전부 미처리로 남았다.
Table of contents
Open Table of contents
원인 — 터진 지점과 진짜 원인
목록 파일을 readlines() 로 읽어 항목 이름 끝에 \n 이 붙었고, 그 이름으로 파일 경로를 만들다가 예외가 났다. Python 공식 문서의 입출력 자습서대로 readline() 은 줄 끝의 개행 문자를 문자열에 남기므로 rstrip() 이나 splitlines() 로 떼야 한다. 처음에는 그 문자를 원인으로 봤다. 지우면 그 자리는 끝나기 때문이다.
다시 보니 진짜 원인은 다른 데 있었다. 어떤 항목에서든 예외는 날 수 있는데 그것이 배치 전체를 죽이는 구조인 것이 문제였다. 이름에 섞인 문자는 이번에 걸린 계기일 뿐이고 다음에는 다른 것이 걸린다. 계기를 원인으로 삼으면 같은 사고가 다른 이름으로 반복된다.
조치 — 고친 방식
항목마다 try·except 로 예외를 잡아 기록하고 continue 로 다음으로 넘어가게 했다. 잡는 자리는 두 가지 방법이 있었는데 process() 안에서 잡아 None 을 돌려주는 것과 루프에서 잡아 results 에 실패 항목을 넣는 것이다. 뒤쪽을 골랐다. 이유는 몇 건을 요청해 몇 건이 됐는지를 세는 자리가 루프라서다. 대신 process() 를 부르는 다른 곳은 여전히 예외가 밖으로 나가는 것을 감수했다. 근거는 그쪽이 한 건만 부르는 자리라 격리할 나머지가 없다는 점이다.
results = []
for item in items:
try:
results.append(process(item))
except Exception as e:
results.append({"item": item, "status": "ERROR", "error": str(e)})
continue
한 건이 실패해도 나머지는 처리되고 실패한 것은 status 가 ERROR 인 항목으로 목록에 남아 나중에 따로 볼 수 있다. 같은 저장소의 다른 파이프라인은 이미 이 형태였고 이 배치만 안 돼 있었으므로 그쪽 패턴에 맞춰 통일했다.
결과 표기
예외를 잡으면 호출한 쪽이 실패를 알아야 한다는 문제가 따라온다. 처음에는 실패한 항목을 results 에서 빼 버렸는데, 그랬더니 백 건 가까이 요청하고 몇 건 모자란 결과를 받은 쪽이 그 차이를 모르고 넘어갔다. 조용히 줄어드는 것이 멈추는 것보다 나쁠 수 있다.
지금은 실패도 결과에 포함하되 상태를 표시하고 마지막에 요청 수와 성공 수를 함께 출력한다. 실패 사유별로 몇 건인지도 센다.
92건 처리 · 성공 87 · 실패 5
- 이름 형식 오류: 3
- 시간 초과: 2
이 형태면 다 됐는지를 따로 세지 않아도 알 수 있고, 사유별로 나뉘어 있으니 어디를 고칠지가 정해진다. 시간 초과는 다시 돌리면 되고 이름 형식 오류는 데이터를 고쳐야 하는 것이라 손댈 곳이 다르다.
재시도의 조건과 발동 횟수
실패를 격리하고 나니 어떤 실패를 다시 시도할 것인가가 다음 질문이 됐다. 재시도 로직을 넣어 둔 배치가 하나 있었는데 그 조건이 한 유형만 보고 있었다. 응답의 json.loads() 실패만 재시도하고 그 뒤 검증 단계에서 난 ValueError 는 재시도 경로에 아예 진입하지 않았다.
더 나빴던 것은 그 배치에서 파싱 실패가 0건이었다는 점이다. 재시도가 한 번도 발동하지 않았으니 그 코드가 제대로 도는지조차 확인된 적이 없었다. 재시도를 넣었다는 사실은 재시도가 동작한다는 증거가 아니다.
그래서 재시도 조건이 실제 실패 유형을 덮는지 확인하고 retry_count 로 발동 횟수를 함께 센다. 0이면 검증된 것이 아니라 한 번도 측정되지 않은 것으로 본다.
판단 기준 — 격리하면 안 되는 자리
전부 격리하는 것이 답은 아니다. 앞 단계가 실패했는데 뒤를 계속하면 잘못된 상태가 쌓이는 경우가 있다. 대상 목록을 만드는 단계가 실패했으면 그 뒤의 처리는 의미가 없으므로 거기서는 raise 로 그대로 멈춘다.
| 구간 | 처리 |
|---|---|
| 항목 단위 처리 | 격리하고 계속 |
| 준비 단계 | 실패하면 중단 |
| 되돌릴 수 없는 조작 | 실패하면 중단 |
세 번째가 특히 중요하다. 파일을 지우거나 외부로 내보내는 조작은 앞이 어긋난 상태에서 계속하면 격리한 만큼 피해가 커진다. 격리는 실패를 안전하게 만드는 장치가 아니라 실패의 영향 범위를 항목 단위로 묶는 장치다.
정리
- 한 항목의 예외가 루프 밖으로 나가면 나머지가 전부 미처리로 남는다
- 걸린 문자나 데이터는 계기일 뿐 예외를 격리하지 않은 구조가 원인이다
- 실패를 결과에서 빼지 말고 상태를 표시한다
- 요청 수와 성공 수를 함께 내고 사유별로 나눠 센다
- 사유별로 나뉘면 어디를 고칠지가 정해진다
- 재시도 조건이 실제 실패 유형을 덮는지 보고 발동 횟수를 센다
- 발동이 0이면 검증된 것이 아니라 측정된 적이 없는 것이다
- 준비 단계와 되돌릴 수 없는 조작은 실패하면 멈춘다