두 글의 주소를 지정하고 그 글만 받아 달라는 작업 기록이 있었다. 결과 폴더에는 요청한 두 글의 저장 메타데이터가 남아 있었다. 그런데 검색순위 필드까지 살펴보니 실제 위치는 비어 있는데 웹에서만 찾은 글이라는 표시와 정렬용 큰 숫자가 함께 있었다. 저장에 성공했다는 말과 검색을 조사했다는 말 사이의 전제를 다시 확인해야 했다.
요청한 범위부터 다시 읽었다
이 작업의 사용자 요청은 검색 결과 전체를 수집하는 것이 아니라 지정한 글만 다운로드하는 것이었다. 당시 결과 요약은 요청 두 건·저장 두 건·실패 없음으로 기록돼 있다. 이 요약은 글을 받은 결과를 설명한다. 검색에서 어느 위치였는지 확인한 결과로 읽어서는 안 된다.
이번에는 글의 원문과 이미지 내용을 내 제작 경험으로 다시 쓰지 않았다. 수집 결과와 메타데이터의 구조, 프로그램의 식별·병합·표시 함수를 읽었다. 공개 본문에는 원래 글의 제목·작성자·주소·이미지를 넣지 않았다. 프로그램이 서로 다른 작업의 성공을 어떻게 표현했는지가 이번 이야기의 재료다.
같은 글을 찾는 키와 발견한 위치는 다른 값이었다
원래 프로그램은 웹과 모바일 주소의 모양이 달라도 게시자 ID와 글 번호를 우선해 같은 글을 연결한다. 제목이 같더라도 글 번호가 다르면 다른 글로 남긴다. 식별자가 없는 예외에는 제목을 정리한 보조 키가 있으며, 이것을 정상 식별자와 같은 확실성으로 읽어서는 안 된다. 이번 합성 모듈은 그 예외를 복사하지 않고 ID를 명시하도록 좁혔다.
같은 글로 연결됐다고 웹 위치와 모바일 위치가 하나가 되는 것은 아니다. 원래 후보 구조는 두 위치와 두 검색 제목·주소·원본 조각을 별도로 둔다. 검색 후보 병합은 글의 중복을 줄이면서 관측의 차이는 남기는 작업이다. 글의 동일성과 관측이 나온 조건을 한 키로 합치지 않는 것이 중요했다.
두 순위가 모두 없을 때의 기본 분기가 드러났다
직접 받은 두 글의 메타데이터에는 웹과 모바일 위치가 모두 null이었다. 그런데 ranking_presence는 WEB_ONLY, best_rank는 10의 9제곱으로 남아 있었다. 같은 필드 묶음을 읽는 사람은 이를 웹에서만 발견했거나 매우 낮은 순위를 확인했다는 뜻으로 오해할 수 있다. 그런 측정을 확인한 기록은 아니었다.
처음 읽은 코드는 두 쪽이 있거나 모바일만 있는 경우를 먼저 처리하고 나머지를 WEB_ONLY로 돌려줬다. 두 쪽 모두 없는 경우가 빠져 있었고 best_rank는 정렬용 큰 숫자를 반환했다. 첫 검토에서는 이 어긋남을 기록하고 합성 예제로 분리했다. 이후 별도의 로컬 개발 작업에서 원래 함수와 저장 문구를 고쳤다. 아래의 후속 기록은 그 수정이며, 과거에 저장한 파일까지 바꿨다는 뜻은 아니다.
미검출이라는 문장에도 조사 여부가 필요했다
순위가 비었다는 사실만으로 조사 범위에서 찾지 못했다고 말할 수 없다. 조사하지 않았을 수도 있고, 페이지를 확인할 수 없었을 수도 있다. 실제로 범위를 읽었지만 그 안에서 찾지 못한 경우와 구별해야 한다. None을 보여 주는 문구 하나가 담당자의 다음 일을 바꾸기 때문이다.
공개 예제는 조사 안 함, 범위 내 미검출, 확인 불가, 위치 관측을 명시적으로 받는다. 0위나 정렬용 대체값을 측정된 위치처럼 받지 않는다. 확인 불가에는 시도하려던 범위를 남길 수 있지만 그 전부를 읽었다는 뜻으로 바꾸지 않는다. 이 네 상태 전체는 합성 구현의 계약이다. 뒤에 설명할 수집기 수정은 값이 없음을 중립적으로 기록하는 범위이며, 이 상세 모델을 통째로 설치한 것은 아니다.
가장 좋은 숫자 하나로 두 관측을 대신하지 않았다
원래 코드의 best_rank는 후보를 정렬할 때 작은 위치를 고르는 부품이다. 그 정렬 기준을 사이트 전체의 대표 순위나 성과 수치로 바로 보여 주면 의미가 바뀐다. 서로 다른 기기와 조사 조건의 값을 어떤 기준으로 비교하는지 먼저 정해야 한다. 이번 글은 현재 검색 노출이나 특정 글의 성과를 측정한 자료가 아니다.
합성 모듈은 두 출처를 그대로 보여 주며 서로 다른 실행·질의 별칭이 섞이면 거절한다. 두 범위가 달라도 각자 남긴다. 별칭이 같다는 것만으로 실제 시간·지역·개인화·조사 방법이 같다는 인증도 생기지 않는다. 같은 글의 자료를 모았다는 편의와 비교 가능한 관측이라는 주장을 따로 두었다.
저장 자료와 통계 자료의 실패도 따로 남긴다
본문을 받았지만 검색순위는 조사하지 않은 경우가 있고, 검색에서 위치를 봤지만 본문은 아직 저장하지 않은 경우도 있을 수 있다. 한쪽이 성공했다고 다른 쪽을 기본값으로 성공시키면 이후 집계가 무엇을 센 것인지 알기 어려워진다. 메타데이터를 다음 도구에 넘길 때도 이런 구분이 유지돼야 한다.
필드별 근거를 다룬 기존 모듈과도 이 질문을 연결했다. 한 필드의 출처가 다른 필드까지 보증하지 않는다는 기준은 재사용할 수 있다. 다만 그 모듈이 실제 검색을 수행하거나 이번 관측 상태 모델과 자동 결합하는 것은 아니다. 각각의 입력 의미와 시험 범위를 읽은 뒤 연결할 부품으로 남긴다.
실제 검색 대신 합성 반례로 상태를 확인했다
최초 콘텐츠 작업은 선택 소스와 과거 요청·결과 메타데이터를 읽는 범위였다. 그때는 수집기를 실행하거나 브라우저·쿠키·계정을 연결하지 않았고 글과 이미지를 다시 내려받지도 않았다. 과거 글 저장 성공을 새 프로그램 회귀시험 성공으로 가져오지 않았다. 관제탑의 오래된 출력 경로 문제도 등록 변경 없이 기록했다. 이후 로컬 개발 검사는 실제 원본 모듈을 읽어 실행한 별도 단계로, 아래에서 구분한다.
공개 예제는 실제로 만든 다섯 합성 기록과 16개 시험을 제공한다. ZIP 추출 뒤에도 같은 시험을 실행하고, 두 언어의 기록 선택기는 DOM 대역에서 확인한다. 실제 검색 화면이나 순위 관측의 검증은 아니다. 독자가 가져갈 것은 높은 숫자를 만드는 방법이 아니라 무엇을 조사했고 무엇은 아직 모르는지 정확히 전달하는 기준이다.
후속 수정에서는 모르는 이유까지 채우지 않았다
빈 순위를 발견한 뒤 곧바로 ‘검색하지 않음’이라는 값으로 바꾸고 싶을 수 있다. 하지만 두 필드가 비었다는 사실은 검색하지 않았다는 증거도 아니다. 나는 실제 수집기에서는 두 순위가 모두 없을 때 UNRECORDED를 반환하도록 바꿨다. 화면과 저장 HTML에는 ‘순위 기록 없음’이라고 썼다. 원인을 모르는 상태에 그럴듯한 이유를 붙이기보다, 확인한 사실만 표현하도록 수정 범위를 좁힌 것이다.
웹 순위만 있으면 WEB_ONLY, 모바일만 있으면 MOBILE_ONLY, 둘 다 있으면 BOTH라는 기존 값은 유지했다. 실제 숫자도 그대로 남긴다. 이 수정은 검색을 새로 수행하거나 조사 범위를 복원하는 기능이 아니다. 운영자가 다음에 확인할 일을 잘못 읽지 않도록, 기록된 위치와 기록되지 않은 위치의 경계를 고친 작업이다.
고객에게는 작은 문구 수정처럼 보일 수 있지만 연결된 도구에는 값의 계약 변경이다. 순위가 반드시 숫자라고 가정한 표나 집계가 있다면 빈 값을 먼저 처리해야 한다. UNRECORDED를 모르는 상태라고 무조건 웹 전용으로 바꾸는 소비자도 같은 오해를 되살린다. 저장하는 쪽만 바꾸고 나머지가 모두 호환된다고 말하지 않은 이유다.
정렬할 때의 대체값을 파일에 저장하지 않았다
best_rank는 기록된 두 위치 중 작은 값을 반환하고, 둘 다 없으면 None을 반환하도록 고쳤다. 그래서 메타데이터 JSON에는 null, CSV에는 빈 칸이 들어간다. 순위가 없다는 뜻을 0위나 매우 큰 순위로 바꾸지 않는다. 파일을 다음 프로그램이 읽어도 이 차이가 남아야 했다.
그렇다고 정렬까지 없애지는 않았다. 병합 정렬 안에서만 순위 없는 항목을 뒤로 보내는 대체값을 사용했다. 그 값은 정렬 키에 머물며 best_rank나 내보낸 파일의 순위가 되지 않는다. 기존에 측정된 순위와 동순위일 때의 보조 정렬 기준을 유지하는 조건으로 변경했다.
이 구분은 통계 화면에도 적용할 수 있다. 빈 잔액을 0원이라고 보여 주거나 확인하지 않은 납기를 오늘로 채우면 화면은 완성돼 보이지만 판단의 근거는 달라진다. 다만 이런 다른 업무에 적용했다는 성과는 이번 기록에 없다. 여기서 확인한 것은 순위 함수와 그 값을 소비하는 저장 경로다.
함수의 반환값에서 실제 저장 파일까지 확인했다
별도의 개발 단계에서 실제 원본 모듈을 import해 오프라인 시험 15개를 실행했다. 순위가 없는 경우, 웹만 있는 경우, 모바일만 있는 경우, 둘 다 있는 경우를 나눴다. 기본 화면 행을 만드는 함수뿐 아니라 순위 CSV와 게시물의 JSON·HTML을 쓰는 실제 메서드에 합성 입력을 넣어 최종 파일을 읽었다. 함수가 None을 반환해도 저장 단계에서 다시 숫자를 채우면 수정은 끝난 것이 아니기 때문이다.
측정 순위가 있는 자료는 이전 버전과도 대조했다. 고정된 난수 입력으로 만든 80개 병합 조합에서 결과 목록과 순서가 이전 코드와 같았다. 게시자와 글 번호의 연결, 두 채널의 위치, 중복 병합 결과도 비교 범위에 들어간다. 이는 검사한 입력 조합의 회귀 근거이며 가능한 모든 검색 결과를 보증하는 수치는 아니다.
이 시험은 브라우저를 띄운 전체 수집 실행이 아니다. 네트워크 연결·자식 실행·GUI 생성을 차단하고, 실제 세션을 만드는 생성자는 건너뛰었다. 저장 메서드에는 이미지가 없는 독립 합성 본문을 전달했다. 따라서 실제 사이트 접근, 이미지 다운로드, 로그인 유지, GUI 화면이나 현재 검색순위가 검증됐다고 읽으면 안 된다.
검사 장치 자체도 한 번 바로잡았다. GUI 시작을 막으려고 Tk 클래스를 함수로 바꾸자 원본 GUI 클래스의 import가 실패했다. 이것은 수집기의 새 결함이 아니라 시험 대역의 오류였다. 생성자만 차단하도록 고친 뒤 원본 편집을 반복하지 않고 검사만 실행했다. 실패 위치를 구분해야 불필요한 제품 수정을 피할 수 있었다.
고친 코드와 과거 기록은 서로 다른 버전으로 남겼다
과거에 받아 둔 결과를 새 규칙으로 조용히 고치지는 않았다. 이전에 남긴 근거와 현재의 메타데이터 세 파일을 해시로 대조했고 그대로임을 확인했다. 과거 파일은 당시 프로그램이 무엇을 썼는지 보여 주는 기록이다. 새 출력 계약과 다르다는 이유로 덮어쓰면 왜 수정했는지 확인할 근거도 사라진다.
수정 전후의 기존 소스 파일을 각각 여섯 개씩 묶어 백업하고, 열두 파일을 별도 로컬 폴더에 실제로 풀어 해시를 비교했다. 복구한 수정본 모듈에서도 같은 15개 시험이 통과했다. 백업 파일이 있다는 확인과 복원된 코드가 같은 검사에 통과한다는 확인을 나눴다. 원래 다운로드 자료를 재수집하거나 운영 프로그램을 재시작한 작업은 아니다.
공통 코드 판단 도구에도 고객 자료를 뺀 작은 함수와 실제 검사 범위를 전달했다. 수정 후에는 최초 판단을 전달하지 않은 별도 요청으로 다시 검토했다. 그 판단은 참고 근거이며 실행 성공을 대신하지 않는다. 최종 파일을 읽고 이전 결과와 대조한 시험, 백업 복구 기록을 함께 남긴 이유다.
그때 남겨 둔 후속 문제는 전체 블로그 수집 경로가 목록 번호를 순위 필드에 쓰는 의미 혼합이었다. 아래의 별도 후속 개발에서 그 호출 경로를 수정했다. 외부 파일 소비자의 호환성을 전부 확인한 것은 아니며, 공개한 네 상태 모듈 전체를 원제품에 설치한 것도 아니다. 시점이 다른 두 수정과 각자의 확인 범위를 구분해 이어서 설명한다.
출력 함수를 고친 뒤에도 입력에서 순위가 만들어졌다
후속으로 전체 블로그 수집 실행기를 읽었더니 글 목록을 넘기는 반복문의 index가 mobile_rank에 들어가고 있었다. 이 경로는 블로그의 글 목록을 받아 본문을 저장하며 검색 결과를 조사하지 않는다. 그런데 저장 엔진에는 양의 정수가 전달되므로, 앞에서 고친 UNRECORDED 분기까지 도달할 이유가 없었다.
실제 main 함수와 실제 게시물 저장 메서드에 두 개의 합성 글을 넣어 확인했다. 목록의 첫째와 둘째 글은 각각 모바일 1위·2위, MOBILE_ONLY, best_rank 1·2로 JSON과 HTML에 저장됐다. 저장 엔진의 규칙이 맞더라도 상위 호출자가 다른 뜻의 숫자를 넣으면 잘못된 정보가 만들어진다. 수정한 함수만 보는 검사에서 입력을 만드는 입구까지 범위를 넓힌 이유다.
목록에서의 위치는 별도 필드로 남겼다
목록 순서를 없애는 대신 list_position이라는 별도 값으로 남겼다. 받은 목록의 행을 복사하면서 1부터 번호를 매기고 글목록.json에 저장한다. 이 번호는 이번 조회 응답에서 그 글이 놓인 자리다. 검색 노출 위치도 아니고, 모든 조회와 실행에서 고정된 번호도 아니다.
수집 후보의 mobile_rank는 None으로 전달한다. 원래 비어 있던 web_rank와 함께 기존 저장 엔진이 두 검색순위를 null, ranking_presence를 UNRECORDED, HTML을 순위 기록 없음으로 표현한다. 실제 검색을 수행하는 GUI와 자동실행 경로, 저장 엔진의 계산·표시 코드는 이번 수정에서 바꾸지 않았다.
이어받기와 실패가 섞이면 이 구분은 더 중요해진다. 먼저 받은 글을 건너뛰거나 한 글 저장에 실패하더라도 목록에서의 위치와 이번에 저장한 순서는 달라질 수 있다. 목록 번호를 복사본에 먼저 붙여 두면 건너뛴 글과 실패한 글도 같은 응답 목록에서 어디에 있었는지 남는다. 원래 전달받은 목록 행을 메모리에서 고치지 않는 것도 시험했다.
전체수집의 호출과 저장을 같은 시험 안에서 이었다
새 시험 12개는 실제 소스에서 꺼낸 main 함수와 기존 저장 메서드를 함께 실행한다. 목록과 본문 전송은 합성 대역으로 바꾸고 네트워크·자식 실행·GUI 시작은 차단했다. 후보의 순위, 저장한 JSON과 HTML, 원본문서와 텍스트, 날짜별 폴더, 빈 목록·실패·이어받기 결과를 읽었다. 실제 플랫폼에서 새 글을 받거나 이미지를 다운로드한 검사는 아니다.
기존 검색 수집 시험 15개도 다시 실행했고, 측정 순위가 있는 80개 병합 조합의 결과를 유지했다. 그래서 새 호출 경로의 12개와 기존 15개를 합친 27개가 통과했다. 수정 전후의 두 변경 파일과 시험 문맥인 저장 엔진을 각각 세 파일씩 묶고, 실제로 푼 수정본에서도 같은 27개 시험을 확인했다. 앞에 설명한 이전 단계의 15개·열두 파일 백업 기록과 다른 후속 실행이다.
이 시험은 main의 함수 코드를 직접 실행하므로 최상위 import 때의 작업 폴더 이동과 의존성 로더는 실행하지 않았다. 실제 브라우저·로그인·목록 API의 현재 응답이나 이미지 저장·GUI 화면을 검수했다는 뜻으로 넓히지 않는다. 저장까지 이어지는 의미 오류를 확인한 범위와 실제 전체 수집 운영의 검수 범위는 함께 적어야 했다.
과거 기록을 보존하는 선택에도 남는 일이 있다
이어받기 시험에는 예전 순위가 적힌 합성 meta.json을 일부러 넣었다. 그 글은 건너뛰면서 기존 파일의 바이트와 수정 시각을 유지하고, 새로 저장한 글만 새 규칙을 따르는지 확인했다. 실제 과거 다운로드를 일괄 고치거나 원본을 다시 받지는 않았다. 기존 기록에는 예전 계약이 남아 있을 수 있다는 한계를 숨기지 않는다.
과거 파일을 새 기준으로 정리하려면 별도 사본과 이관 기준이 필요하다. 순위 숫자만 보고 목록 번호였는지 실제 검색 관측이었는지 전부 알아낼 수 없기 때문이다. 값을 비우는 변환을 자동 적용하기보다 당시 호출 경로·기록과 근거를 먼저 확인해야 한다. 이번 수정은 과거 기록의 의미를 인증하거나 목록 API의 완전성을 증명하지 않는다.
공유 모듈의 not_performed도 이유를 아는 호스트가 명시적으로 전달해야 한다. 이번 전체수집 경로가 검색을 호출하지 않는다는 근거와 일반적인 UNRECORDED 한 값은 정보의 양이 다르다. 이 둘을 연결하는 새 어댑터는 구현하지 않았다. 나는 결과 필드가 비어 있다는 사실보다 그 값이 만들어지는 경로를 확인하는 일을 이번 후속의 핵심으로 남겼다.