Devin.KR

수집과 원본 보존

110분 안팎

학습 목표

로컬 HTTP 모의 서버의 공개 데이터 응답을 받아 원본과 체크섬을 보존하고 시간 초과를 처리합니다.

개념

수집은 변환 이전의 증거를 남기는 일입니다

교통 보고서가 어제와 달라졌을 때 새 원본이 달라졌는지 정제 코드가 달라졌는지 구분하려면 수집한 응답이 남아 있어야 합니다. JSON을 읽자마자 날짜를 고치고 다시 저장하면 원래 날짜 표기를 확인할 수 없습니다. 이 레슨의 수집 함수는 응답 바이트를 먼저 확보하고 성공 여부를 판정한 뒤 그대로 보존합니다. 분석용 자료형 변환은 다음 단계에 맡깁니다.

기본 미션은 인터넷 없이 joined.csv를 읽습니다. 선택 수집 함수 collect_http는 공개 API 형태의 JSON을 받는 연습용 입구입니다. 두 입구의 결과 파일은 각각 payload.csv와 payload.json입니다. HTTP JSON을 기본 ETL의 CSV로 자동 변환하지 않습니다. 실제 출처를 연결할 때에는 그 출처의 필드와 관측 단위를 확인하고 JSON에서 결합 CSV를 만드는 별도 어댑터를 작성해야 합니다.

바이트와 해석한 값을 구분합니다

같은 JSON 객체라도 공백과 줄바꿈이 다르면 파일 바이트가 다릅니다. 수집 체크섬은 객체의 의미를 요약하는 것이 아니라 받은 바이트의 변경을 감지합니다. response.read로 얻은 bytes를 SHA-256에 넣고, JSON 구문 검사는 그 바이트를 UTF-8로 해석해 수행합니다. json.dumps로 다시 저장하면 공백이나 키 순서가 달라질 수 있으므로 원본 보존에는 사용하지 않습니다.

보존 폴더 이름은 바이트의 SHA-256 값입니다. 그 안에 payload와 manifest.json을 두며 manifest는 최초 출처, 파일 이름, 바이트 수, 해시를 기록합니다. 같은 바이트를 다시 수집하면 기존 폴더를 반환하며 처음 기록을 덮어쓰지 않습니다. 이 단순 실습의 manifest는 최초 수집 증거이고 모든 요청 이력을 담는 로그는 아닙니다. 요청마다의 시각·상태 이력은 운영 설계에서 별도로 추가합니다.

해시가 같다고 내용이 정확하거나 출처가 진짜라는 뜻은 아닙니다. 틀린 관측도 똑같이 다시 받으면 해시가 같습니다. 원본 바이트의 변경 확인, 제공자의 설명과 출처 확인, 값의 품질 검사는 서로 다른 책임입니다. 이전 파일과 수치가 달라지면 먼저 해시를 비교하고 바뀐 입력의 행과 열을 확인한 뒤 정제·지표 코드를 검토합니다.

성공한 수집만 공개합니다

파일을 쓰기 전에 크기를 제한하고 JSON 구문을 확인합니다. 실습 상한은 1MiB이며 LIMIT보다 한 바이트 더 읽어 상한 초과를 알아냅니다. UTF-8로 해석할 수 없거나 JSON 문법이 깨졌다면 JSON 오류로 멈추고 보존 결과를 게시하지 않습니다. 정상 JSON인 배열이나 객체 안의 필수 열 검사까지 수집이 담당하지는 않습니다. 구문 성공과 업무 스키마 성공을 분리합니다.

보존 과정에서는 같은 부모 폴더 아래 임시 디렉터리에 파일과 manifest를 모두 작성하고 마지막에 내용 주소 폴더로 이름을 바꿉니다. 작성 도중의 불완전 파일을 완성된 수집처럼 읽는 일을 줄입니다. 이 실습은 한 수집 작업을 순차 실행하는 조건입니다. 동시에 같은 내용을 저장하는 여러 실행, 디스크 장애와 전원 차단까지 안전하다고 주장하지 않습니다. 임시 경로는 finally에서 정리합니다.

동일 해시 폴더가 이미 있으면 저장된 payload의 바이트를 다시 비교합니다. 기존 파일이 바뀌었다면 INTEGRITY 오류를 내고 정상 수집으로 처리하지 않습니다. 해시처럼 보이는 폴더 이름을 신뢰하는 것만으로는 손상 탐지가 되지 않습니다. 이 함수의 검사 범위는 payload이며 manifest에 대한 서명이나 진위 확인까지 제공하는 것은 아닙니다.

오류의 종류를 읽고 다음 행동을 정합니다

urlopen에는 양수의 유한한 timeout을 전달합니다. 이 시간은 소켓 작업의 대기 제한이며 수집 전체가 정확히 그 초 안에 끝난다는 총 실행 시간 계약은 아닙니다. 요청 연결 중 또는 응답을 읽는 중 기다림이 길어지면 TimeoutError가 날 수 있고 URLError의 reason에 시간 초과가 담길 수도 있습니다. 함수는 이 경우 TIMEOUT으로 정리해 호출자에게 알립니다.

HTTP 503은 서버가 응답한 오류이고 NETWORK는 연결이나 주소 해석 단계의 실패입니다. HTTPError가 URLError의 하위 예외이므로 먼저 잡아 상태 코드를 남깁니다. 404를 네트워크 오류처럼 숨기면 담당자가 주소 오타 대신 네트워크부터 조사할 수 있습니다. 실습 함수는 자동 재시도를 하지 않습니다. 오류를 알리고 멈춰 실패 경계부터 이해하며 재시도 간격과 횟수는 운영 정책에서 정합니다.

잘못된 JSON을 빈 목록으로 바꾸면 장애가 정상적인 빈 데이터 수집으로 보입니다. None을 반환하고 뒤 단계가 조용히 종료되는 방식도 원인을 지웁니다. JSON: UTF-8 JSON 필요 같은 명시적 예외로 실패를 보존합니다. 반면 유효한 빈 JSON 배열은 구문 오류가 아닙니다. 빈 관측을 허용할지 거부할지는 후속 스키마와 품질 계약에서 결정합니다.

응답 주입과 실제 HTTP 검사를 구분합니다

기본 테스트는 urllib 응답 객체를 주입해 수집 분기를 검사합니다. 성공, 지연 예외, HTTP 오류, 잘못된 JSON, 크기 초과, 재수집과 기존 파일 손상을 확인합니다. 이는 포트가 막힌 환경에서도 실행할 수 있지만 실제 소켓 통신을 검증하지는 않습니다. 별도 통합 테스트는 127.0.0.1의 임시 포트에서 모의 서버를 열고 같은 수집 함수를 호출합니다.

macOS와 Linux에서 BOOTCAMP_HTTP=1을 붙여 테스트를 실행하면 통합 검사가 활성화됩니다. 모의 서버는 성공 응답, 지연 응답, 잘못된 JSON과 503을 만들며 테스트가 끝나면 shutdown, server_close, thread.join으로 정리합니다. PermissionError: Operation not permitted가 포트 생성에서 나면 코드의 JSON 처리가 아니라 실행 환경의 통신 권한을 확인합니다. 이 경우 기본 검사 통과를 HTTP 통합 성공으로 보고하지 않습니다.

실제 공개 출처 수집은 제공자의 사용 조건과 관측 기간을 확인한 다음 선택합니다. 인증 키가 필요하면 원고·소스·manifest에 넣지 않고 별도 설정으로 다룹니다. 실습 manifest의 source는 인증 정보 없는 모의 URL만 담습니다. 더 읽기는 페이지 처리와 재시도를 확장해 설명합니다. 여기서는 원본 바이트를 바꾸지 않는 보존과 실패를 성공으로 만들지 않는 오류 처리를 완성합니다.

따라하기

바이트 그대로 해시 비교

공백 한 칸도 원본 바이트에 영향을 줍니다. 같은 객체 의미와 같은 체크섬을 구분합니다.

import hashlib
a = b'{"rain_mm":0}'
b = b'{"rain_mm": 0}'
print('same_bytes', a == b)
print('same_hash', hashlib.sha256(a).digest() == hashlib.sha256(b).digest())

실행 결과

same_bytes False
same_hash False

JSON 구문과 업무 필드 구분

region만 있는 객체도 JSON 구문은 정상입니다. 필수 숫자 열은 다음 스키마 검사에서 확인합니다.

import json
for text in ['{"region":"A"}', 'not json', '[]']:
    try:
        data = json.loads(text)
        print('JSON_OK', type(data).__name__)
    except json.JSONDecodeError:
        print('JSON_INVALID')

실행 결과

JSON_OK dict
JSON_INVALID
JSON_OK list

보존 함수 직접 실행

수집 파일과 manifest가 같은 바이트를 가리키는지 확인합니다. 실습 ZIP의 함수는 임시 디렉터리 게시와 기존 원본 손상 검사까지 추가합니다.

import tempfile, pathlib, hashlib, json
with tempfile.TemporaryDirectory() as folder:
    data = b'region,date\nA,2026-09-01\n'
    root = pathlib.Path(folder)
    digest = hashlib.sha256(data).hexdigest()
    target = root / digest
    target.mkdir()
    (target / 'payload.csv').write_bytes(data)
    (target / 'manifest.json').write_text(json.dumps({'sha256':digest,'bytes':len(data)}))
    restored = (target / 'payload.csv').read_bytes()
    manifest = json.loads((target / 'manifest.json').read_text())
    print('preserved', restored == data)
    print('checksum_matches', hashlib.sha256(restored).hexdigest() == manifest['sha256'])
    print('bytes', manifest['bytes'])

실행 결과

preserved True
checksum_matches True
bytes 25

ZIP 검사와 실패 위치 확인

starter.zip을 푼 루트에서 아래 명령을 실행합니다. 기본 응답 주입 9개 테스트 통과, HTTP 통합 9개는 기본 건너뜀입니다. starter는 잘못된 JSON 검사 1개 실패입니다. 실패의 actual과 expected를 읽고 TODO를 채운 뒤 다시 검사합니다. HTTP 통합은 이 작성 샌드박스의 포트 권한 때문에 실행하지 못했으며 기본 검사와 구분합니다.

python3 -m unittest discover -s tests -v

확인 문제

실습

collect_http에서 잘못된 UTF-8 JSON을 CollectionError로 알리도록 수정합니다. 원본 게시를 하지 않아야 하며 성공 응답은 바이트·manifest·재수집 검사를 통과해야 합니다. 기본 명령은 응답 주입 검사입니다. BOOTCAMP_HTTP=1을 붙이면 실제 모의 서버 통합 검사도 수행하며 포트가 허용된 환경에서 실행합니다. 이 작성 환경에서는 실제 HTTP 통합은 미확인입니다.

bash check.sh는 기본 9개와 모의 서버 통합 9개를 함께 실행해 바이트·체크섬·재수집·손상, HTTP 오류·시간 초과·JSON 반려 시 미게시를 확인합니다.

시작 코드·테스트 내려받기

실행 명령

bash check.sh

기대 결과

포트가 허용된 환경에서 기본 단위 9개와 실제 HTTP 통합 9개 통과입니다. 기본 단위 검사 9개는 작성 환경에서 통과했고 실제 HTTP는 PENDING입니다.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 보고서의 수치와 원본 데이터가 다를 때 확인할 순서를 설명해 주시면 됩니다.