오류 하나를 만났을 때 프로그램이 계속해야 하는지 멈춰야 하는지는 단순한 성격 문제가 아니었다. 무엇이 잘못됐고 다른 입력의 의미가 여전히 분명한지에 따라 경계가 달랐다. 블로그 수집기의 주소 처리 세 곳을 고치며 그 기준을 실제 저장 결과까지 확인했다.

한 번의 실패가 어디까지 영향을 줘야 할까

본문은 읽을 수 있는데 이미지 주소 하나가 잘못됐다면 글 전체의 저장을 포기해야 할까. 반대로 오류를 모두 무시하고 저장 완료라고 적어도 될까. 작업 기록에 남은 주소 해석 오류를 따라가며, 나는 계속 처리할 범위와 반드시 남길 실패를 함께 정했다.

기존 수집기에는 게시물을 식별하는 주소, 이미지 후보 주소, 본문 안의 연결 주소가 있었다. 닫히지 않은 괄호가 호스트 부분에 있는 합성 입력을 각각 넣자 세 경로 모두 ValueError로 중단됐다. 실제 과거 실패 글을 다시 받은 실험은 아니다. 같은 오류 문구를 현재 함수에서 재현한 것이며, 과거 실패가 전부 같은 원인이라고 단정하지 않았다.

다음 후보는 새로운 요청이 아니라 남아 있던 입력이었다

이미지 한 장에도 주소 후보가 여러 개 있을 수 있다. 기존 코드는 지연 로딩 속성, 일반 src, srcset, JSON 속성에서 후보를 만든 뒤 순서대로 살폈다. 나는 그 후보 생성 규칙을 새 표준으로 바꾸지 않고 주소를 합치는 작은 경계에 예외 처리를 넣었다. 앞 후보가 해석되지 않으면 이미 준비된 다음 후보를 살펴보게 했다.

예를 들어 앞 후보의 괄호가 잘못됐지만 뒤에 정상 상대주소가 있다면, 뒤 후보를 기준 주소와 합쳐 선택할 수 있다. 잘못된 문자를 지우거나 다른 주소를 추측해 만드는 방식은 쓰지 않았다. 앞 후보를 고쳐 준 것이 아니라 그 후보를 해석할 수 없다는 사실을 남기고 별도의 입력을 선택한 것이다. 후보 순서는 여전히 원래 수집기의 책임이다.

예외를 잡는 위치를 작게 유지했다

글 전체를 감싼 큰 try 안에서 모든 오류를 무시하면 저장 경로 오류나 다른 프로그래밍 실수까지 지나칠 수 있다. 이번 수정은 주소 해석에서 발생하는 ValueError만 해당 경계에서 처리한다. 글 식별에서는 미확인을 돌려주고, 이미지 후보에서는 다음 후보로, 본문 링크에서는 주소 속성 제거로 이어진다.

같은 예외라도 다음 행동은 달랐다. 게시물 식별 함수가 None을 반환했다고 해서 새 식별자를 만들어 주지는 않는다. 그다음 호출자가 식별 불가를 어떻게 처리할지는 원래 흐름의 책임이다. 모든 예외를 같은 빈 문자열로 바꿔 놓으면 어느 단계가 무엇을 몰랐는지 사라진다.

속성 두 개의 오류를 이미지 두 장의 실패로 세지 않았다

한 이미지의 앞 후보 두 개가 잘못돼도 뒤 후보를 선택해 다운로드할 수 있다면 이미지 주소 해석은 결국 해결됐다. 이때 앞선 후보 오류를 이미지 실패 수에 더하지 않았다. 반대로 후보를 모두 살펴도 선택할 주소가 없고 해석 오류가 남아 있다면 그 이미지를 실패 한 건으로 세고 다음 이미지와 본문 저장을 이어 갔다.

따라서 후보별 진단과 이미지별 집계는 단위가 다르다. 지원하지 않는 data 또는 blob 주소를 기존 규칙대로 제외한 것만으로 해석 실패를 만들지도 않았다. ‘오류가 한 번이라도 있었음’과 ‘해결하지 못한 이미지가 있음’을 분리해야 실패 수가 실제 다음 작업을 가리킬 수 있다.

연결을 지워도 읽을 문장은 남길 수 있었다

본문 정리에서는 잘못된 연결 주소의 href만 제거했다. 연결을 담은 글자와 주변 문단, 정상 연결은 남겼다. 주소를 해석할 수 없다는 이유로 문장 전체를 없애면 저장 결과의 정보가 더 크게 줄어들기 때문이다. 이미지를 처리하다 생긴 실패 역시 본문 텍스트 저장의 성공과 함께 기록할 수 있어야 했다.

이 처리가 저장 HTML의 모든 깨진 이미지 표시를 정리하거나 링크의 업무 의미까지 검사하는 것은 아니다. 이번에 확인한 것은 잘못된 href가 글 전체 정리를 멈추지 않고 읽을 문자가 남는다는 동작이다. 실패가 사라졌다고 말하기보다 무엇을 보존하고 무엇을 쓸 수 없게 했는지 설명하는 편이 정확했다.

주소 선택과 다운로드 재시도는 다른 기능이었다

여기서 다음 후보로 넘어가는 조건은 주소 해석 실패다. 첫 번째로 해석한 HTTP 계열 주소를 골랐는데 실제 다운로드가 실패하면, 이번 수정은 나머지 속성을 다시 순회하지 않는다. 기존 다운로드 경로가 이미지 실패를 기록한다. 이 차이를 빼고 ‘이미지를 받을 때까지 자동으로 다른 주소를 찾는다’고 설명하면 구현보다 큰 약속이 된다.

주소가 해석된다는 사실은 접근해도 안전하다는 판단도 아니다. 허용한 호스트와 포트, 인증, 리다이렉트, 응답 크기와 이미지 내용은 별도로 다룰 책임이다. 이번 코드는 주소 신뢰 정책을 완성한 수정이 아니며 외부 접근 제한을 우회하지 않는다. 계속할 수 있는 작은 실패와 전체 작업을 멈춰야 할 설정 문제를 같은 규칙으로 만들지 않았다.

정상 입력을 남기는 검사도 함께 했다

별도의 로컬 개발 단계에서 새 URL 회귀 14개와 기존 검색순위 15개, 전체목록 12개가 통과했다. 순위 병합 80개 조합도 유지했다. 상대주소, 정상 IPv6, 쿼리 안의 괄호, 중복 이미지와 사용자 중지 같은 기존 동작을 함께 확인했다. 나쁜 괄호를 막겠다고 정상 주소의 괄호까지 지우는 수정을 피하기 위해서였다.

실제 함수의 이미지 처리와 본문 정리에서 JSON·HTML·텍스트 저장까지 합성 입력을 연결했다. 네트워크와 이미지 전송은 대역이며 사진 품질이나 이미지 디코딩을 검사한 것은 아니다. 전체목록의 main 검사는 최상위 시작 코드를 생략한 AST 기반이었다. 이 41개와 복구 전후 27개·41개는 앞선 개발 기록이며 이번 집필 단계에서 원제품을 다시 실행하지 않았다.

예전 실패를 고쳤다는 말은 별도 근거가 필요했다

수정 전후 소스를 각각 묶어 실제로 풀고 해시를 대조한 다음, 복구한 소스에서 해당 회귀 검사를 실행했다. 고친 함수 외의 소스와 기존 실행 자료는 보존했다. 그러나 새로운 반례가 통과했다는 사실만으로 과거에 실패한 글과 이미지까지 복구됐다고 적지는 않았다. 과거 원문과 현재 응답을 다시 확인하는 일은 별개의 작업이다.

이번 공개 자료에는 그 글의 제목·작성자·주소·이미지를 넣지 않았다. 실패를 재현하는 데 필요한 것은 고객 원고가 아니라 해석할 수 없는 후보와 그 뒤의 정상 후보라는 구조였다. 수집 프로그램을 개선한 경험을 공개하면서도 원래 자료와 멈춰 둔 감시 작업을 그대로 두었다.

재사용 예제는 주소보다 선택 이유를 돌려준다

기존 CSV 모듈은 파일 전체의 구문과 행 오류를 나누고, 목적지 비교 모듈은 기대한 이동 결과를 확인한다. 이 주소 후보 반복을 대신하는 구현은 아니었다. 그래서 별도 합성 모듈은 최대 32개 문자열 후보와 기준 주소를 받아 선택 번호, 해석 오류 번호, 제외 사유와 해결되지 않은 항목 신호만 돌려주도록 작게 만들었다.

공유 예제는 원문 주소를 결과에 넣지 않고 네트워크 요청도 하지 않는다. 이번에는 그 소스와 ZIP 추출본에서 각각 20개 시험을 실행하고 여섯 합성 기록을 생성했다. 글 안의 선택기는 저장된 기록을 읽으며 Python을 실행하지 않는다. 이 시험과 원제품의 41개 검사는 서로 다른 대상이다. 실제 업무에 붙일 때는 후보 생성, 접근 정책, 전송·저장 결과와 집계 단위를 따로 연결하면 된다.