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