전체 흐름 회귀 검증
65분 안팎
학습 목표
정상·경계·오류·재실행을 함께 검증합니다.
개념
기능을 연결한 약속을 확인합니다
독서 기록의 입력 함수와 저장 함수가 각각 통과해도, 추가 버튼에 해당하는 진입점에서 저장 호출을 빠뜨리면 다음 실행에 기록이 사라집니다. 마지막 모듈에서는 새 기능의 양보다 기존 기능을 연결한 결과를 확인합니다. 회귀 검증은 변경 뒤에도 이전에 약속한 동작이 유지되는지 다시 검사하는 일입니다. 통합 검증은 입력부터 저장·재실행·보고서까지 연결된 흐름을 확인하는 일입니다. 두 관점을 함께 사용해야 인계받은 사람이 첫날부터 문제를 재현할 수 있습니다.
이번 로컬 실습은 Python 3.9 이상과 Bash를 사용하며 표준 라이브러리만 필요합니다. ZIP을 풀고 handoff.py가 있는 폴더에서 명령을 실행합니다. 가상 자료만 사용하며 기존 개인 기록을 가져오지 않습니다. 앞 모듈 미션 solution의 모든 코드를 이어받았습니다. reports 폴더의 빈 표시 파일도 전달하여 첫 작업 공간 검사가 가능하게 합니다. check.sh는 초기 모듈의 검사이고 이번 레슨 명령은 bash check-regression.sh입니다. 최종 미션의 전체 명령은 bash check-final.sh로 구분합니다.
기대 결과를 요구에서 정합니다
테스트의 정답을 지금 구현의 출력으로 정하면 저장 누락도 정답이 될 수 있습니다. b01을 제목 ‘책, 산책’, 쪽수 7, 금액 100, 완료 1로 추가하면 상태는 OK이고 JSON 파일이 존재해야 합니다. 새 Python 프로세스에서 b01을 조회하면 같은 제목과 수치가 나와야 합니다. 여기에 b02를 0쪽·미완료로 추가하면 공개 보고서는 두 행, 완료 목록 한 행, 완료 쪽수 합계 7이어야 합니다. 합계는 모든 기록의 쪽수가 아니라 완료한 기록의 쪽수라는 기존 SQL 계약을 사용합니다.
공개 결과는 id·title·pages·completed만 포함합니다. 금액과 개인 식별 필드는 공개하지 않습니다. total_pages가 7인지 확인하는 것과 각 행의 열 집합이 정확한지 확인하는 것은 다른 질문입니다. 값만 맞으면 민감한 필드가 더 붙은 결과도 통과할 수 있으므로 열 집합까지 대조합니다. 이 검사는 현재 보고서 구조의 공개 계약을 확인하며 향후 추가한 모든 문서나 로그의 비공개를 보증하지는 않습니다.
성공 뒤에 무엇이 남아야 하는지 정합니다
handoff.add_saved는 저장 경로와 다섯 입력 문자열을 받습니다. load_store로 기존 JSON을 읽고, store.add_record로 후보 저장소에 기록을 추가합니다. status가 OK일 때만 save_store를 호출하고 상태를 돌려줍니다. 기존 추가 함수의 검증을 다시 작성하지 않는 이유는 오류 우선순위와 정수 입력 계약을 유지하기 위해서입니다. 메모리 수정과 파일 저장의 경계를 여기서 연결하되 CLI의 import·lookup·report 동작은 바꾸지 않습니다.
starter는 후보를 만들고 OK를 반환하지만 파일에 저장하지 않습니다. 여섯 흐름 테스트 중 CSV 왕복은 기존 구현으로 통과하고 새 저장 연결이 필요한 테스트들은 실패합니다. seed는 테스트를 준비하는 함수이며 파일 존재 확인이 먼저 실패하면 뒤의 조회 비교에는 아직 도달하지 않은 것입니다. 이 경우 다섯 테스트 이름이 실패해도 다섯 종류의 독립적인 버그라고 단정하지 않습니다. 첫 공통 실패 위치를 읽고 누락한 save_store 호출을 찾습니다.
실패 상태와 원본 보존을 함께 검사합니다
중복 ID b01은 ERR_DUPLICATE_ID이고 앞뒤 공백이 있는 ‘ b01 ’도 같은 ID입니다. 빈 제목은 ERR_TITLE, 문자 금액 ‘원’은 ERR_COST_NOT_INTEGER, 음수 쪽수는 ERR_PAGES_NEGATIVE입니다. 실패 전에 JSON의 read_bytes 결과를 보관하고 호출 뒤 같은 바이트인지 비교합니다. 상태 문자열만 확인하면 오류를 알리면서 파일도 바꾸는 결함을 놓칠 수 있습니다. 보존 대상과 보존 조건을 각각 테스트에 적습니다.
CSV에서는 첫 행이 정상이고 두 번째 행의 금액이 문자인 경우를 넣습니다. 행마다 바로 저장하면 첫 행까지만 파일이 바뀔 수 있기 때문입니다. persistence.read_csv는 후보를 모두 검증한 뒤 save_store로 넘어갑니다. test_csv_late_failure_keeps_bytes는 ValueError 발생과 이전 파일 바이트 유지 둘 다 확인합니다. 실패 입력에 임의로 0을 넣어 계속 처리하면 사용자 실수와 실제 0원 기록이 섞이므로 현재 계약에서는 거절합니다.
빈 입력을 한 가지 뜻으로 묶지 않습니다
제목 공백은 잘못된 기록이고 0쪽은 허용된 기록입니다. 헤더만 있는 CSV는 유효한 빈 목록이며 import는 전체 교체라 기존 기록을 빈 목록으로 바꿉니다. CSV 파일 자체가 없는 상황은 파일 오류입니다. 이 네 상황은 모두 비어 보이지만 기대 결과가 다릅니다. test_empty_header_replaces_all은 빈 목록과 빈 공개 보고서, 합계 0을 확인합니다. 테스트를 보고 기존 기록을 지키려면 빈 CSV를 전부 거절해야 한다고 해석하지 않습니다.
한글 CSV 왕복에는 제목 안의 쉼표도 포함합니다. CSV에서 제목을 따옴표로 묶으면 쉼표를 필드 구분자가 아닌 글자로 읽을 수 있습니다. 가져온 후보 저장소와 JSON을 다시 읽은 저장소를 비교하며 제목이 ‘책, 산책’인지 별도로 확인합니다. JSON의 들여쓰기나 키 순서는 데이터 의미와 분리합니다. 같은 값이 복원되는지를 볼 때는 객체 비교가 적합하고 실패 후 원본을 지키는지 볼 때는 바이트 비교가 적합합니다.
실행 범위를 결과와 같이 전달합니다
check-regression.sh는 최종 흐름 여섯 테스트 다음 clean_run.py를 실행합니다. clean_run.py는 생성 파일 없이 새 폴더로 필요한 Python·SQL·CSV를 복사하고 import·lookup·report를 수행합니다. 따라서 이 명령은 저장 연결과 새 폴더 파일 재현의 근거입니다. HTTP나 Git 기록까지 확인했다는 뜻으로 확대하지 않습니다. 미션의 check-final.sh --files는 기존 HTTP 이외 테스트까지 합쳐 실행하며 전체 check-final.sh는 HTTP 소켓 검사를 포함합니다.
FAIL은 기대 비교가 어긋난 경우이고 ERROR는 검사 중 예상하지 못한 예외가 난 경우입니다. AssertionError 뒤의 ‘OK를 반환했지만 JSON 파일이 없습니다’는 파일 존재 계약 위반을 가리킵니다. ModuleNotFoundError라면 압축을 푼 위치와 누락 파일부터 확인합니다. PermissionError가 HTTP bind에서 나면 파일 저장 버그와 구별해 외부 실행 환경에서 다시 검사합니다. 검증 실패를 숨기려고 테스트를 삭제하거나 종료 코드를 0으로 바꾸지 않습니다.
제출에는 명령, 실행 범위, 테스트 수, 실패 수, 오류 수, 종료 코드와 새 폴더 표시를 남깁니다. 실행 시간은 환경마다 달라 고정 정답으로 쓰지 않습니다. 서버를 쓰는 전체 검사는 외부 검증 대기로 표시하며 파일 검사 통과를 그 대체 증거로 적지 않습니다. 이 레슨을 마치면 저장 누락을 재현하고 수정한 뒤 정상·경계·오류·재실행 중 무엇을 확인했는지 말할 수 있어야 합니다. 테스트를 반례로 생각하는 일반 연습은 더 읽기에서 이어갑니다.
따라하기
메모리 성공과 파일 존재 구분
독립된 작은 재현입니다. 후보 딕셔너리에 추가한 사실과 디스크 파일이 생긴 사실을 각각 확인합니다. 실제 프로젝트에서는 handoff.add_saved를 고칩니다.
from tempfile import TemporaryDirectory
from pathlib import Path
with TemporaryDirectory() as d:
store = {}
store["b01"] = {"title": "책, 산책", "pages": 7}
print("memory", len(store))
print("disk", (Path(d) / "records.json").exists())실행 결과
memory 1 disk False
CSV 쉼표와 한글 보존
필드 안의 쉼표가 한글 제목의 일부로 읽히는지 확인합니다. 프로젝트의 실제 파일 왕복은 test_korean_csv_roundtrip이 검사합니다.
import csv, io
source = 'id,title,pages,cost,completed\nb01,"책, 산책",7,100,1\n'
row = next(csv.DictReader(io.StringIO(source)))
print(row["title"])
print(int(row["pages"]))실행 결과
책, 산책 7
값 비교와 바이트 비교 구분
두 JSON은 자료로 같지만 원본 바이트는 다릅니다. 왕복 검사는 값, 실패 입력 이후 원본 유지 검사는 바이트를 비교합니다.
import json
before = '[{"pages":7}]\n'
after = '[ { "pages": 7 } ]\n'
print("same_value", json.loads(before) == json.loads(after))
print("same_bytes", before.encode() == after.encode())실행 결과
same_value True same_bytes False
저장 연결 수정과 검사
handoff.py의 OK 분기에서 pass를 save_store(path, store)로 바꾼 뒤 실행합니다. 여섯 흐름 테스트의 OK와 CLEAN|import|lookup|report|PASS를 확인합니다. 실습 폴더에서 수행하는 명령입니다.
bash check-regression.sh확인 문제
실습
앞 모듈 프로젝트를 보존하고 handoff.py의 add_saved를 완성합니다. 정상 상태에만 저장하고 중복·빈 제목·문자 금액·음수 쪽수에서는 원본을 지킵니다. 제공한 tests/test_final_flow.py를 유지하며 저장 후 별도 프로세스 조회, 공개 열, CSV 왕복, 빈 CSV 전체 교체와 새 폴더 검사를 통과합니다.
실행 명령
bash check-regression.sh
기대 결과
최종 흐름 6개 OK, CLEAN|import|lookup|report|PASS, 종료 코드 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 다른 사람이 프로그램을 실행하도록 준비한 내용을 설명합니다.
- 메모리와 디스크에 저장된 데이터의 차이를 설명합니다.