체크포인트 저장하기
100분 안팎
학습 목표
종료 코드·검사 시각·파일 해시를 수집해 상태 기록과 연결하는 로컬 스크립트를 작성합니다.
개념
통과 문장에는 어느 파일인지 빠져 있습니다
어제 테스트가 통과했다고만 적으면 오늘 코드가 바뀌어도 그 문장을 믿을 수 있습니다. 하네스는 검사한 입력과 관찰을 묶는 체크포인트를 만듭니다. 할 일 앱의 생성 순서 검사라면 Java 코드뿐 아니라 테스트·명세·의존성 설정도 검사 결과에 영향을 줍니다. 체크포인트는 결과의 근거를 찾기 위한 기록이며 앱 기능을 새로 구현하는 단계가 아닙니다. 이번 로컬 과제는 합성 task.txt에서 기록 형식을 익히고 미션에서 실제 앱 파일 목록에 연결합니다.
기록은 검사 전부터 시작합니다
검사 명령을 호출하기 전에 state.json에 running을 저장합니다. 작업 도중 세션이 끝나면 새 담당자가 완성된 기록과 중단된 기록을 구별할 수 있습니다. 검사 이후에만 상태를 저장하면 시작했는지조차 남지 않을 수 있습니다. 미션의 checkpoint.py는 준비된 작업 하나를 고른 뒤 running을 저장하고 capture를 호출합니다. 작업이 준비되지 않으면 NOT_READY를 출력하고 명령을 호출하지 않습니다. 순서가 맞는 계획이라는 사실과 선행 작업이 현재 검증되었다는 사실은 각각 확인합니다.
명령은 문자열 설명과 실제 인자를 연결합니다
capture에는 실행할 인자 배열 argv를 넘기며 subprocess.run으로 완료까지 기다립니다. 검사 이름은 미션의 commands 함수에서 실제 명령 배열에 연결합니다. JSON 안의 설명을 셸 명령으로 바로 실행하지 않습니다. argv를 기록하면 python3 verify_app.py를 실행했는지 다른 스크립트를 실행했는지 재개 시점에 대조할 수 있습니다. 이 레슨의 등록 목록은 교육용 검사 선택이며 입력 경로와 외부 명령을 제한하는 전체 보안 정책은 아닙니다. 다음 모듈에서 권한 경계를 별도로 구성합니다.
종료 코드는 자식 명령에서 가져옵니다
subprocess.run이 정상 호출되었다고 자식 검사가 통과한 것은 아닙니다. CompletedProcess.returncode를 읽습니다. 종료 코드 0이면 passed, 0이 아니면 failed입니다. 명령 파일이 없어서 OSError가 발생하면 exitCode는 null이고 result는 unverified입니다. 실제로 실행되어 기대 비교에 실패한 경우와 실행 자체를 하지 못한 경우를 구분합니다. starter의 결함은 자식 결과와 관계없이 코드 0을 저장합니다. 실패 명령이 7로 종료하는 테스트를 통해 이 잘못을 검출하고 실제 반환값을 보존하도록 고칩니다.
실패 이후에도 기록을 남깁니다
셸에 set -e를 켠 상태에서 실패 명령을 그대로 실행하면 결과를 저장하기 전에 스크립트가 끝날 수 있습니다. 여기서는 Python capture가 자식 종료 코드를 받아 기록하고, checkpoint.py가 상태 파일을 저장한 뒤 실패 종료를 반환합니다. 바깥 check.sh의 set -eu는 관문 실패 시 이후 검사를 중단하는 역할입니다. 둘은 적용 위치가 다릅니다. 테스트 이름과 기대·실제가 들어 있는 로그를 보존해야 다음 담당자가 실패를 재현할 수 있으며 종료 코드만으로 원인을 확정하지 않습니다.
시각은 UTC 오프셋을 포함합니다
startedAt은 검사 직전, checkedAt은 결과를 수집한 뒤 기록한 ISO 형식의 UTC 시각입니다. 테스트는 시각 문자열이 존재하고 시간대가 있으며 시작보다 종료가 빠르지 않은지 확인합니다. 특정 날짜 문자열을 정답으로 고정하지 않습니다. 시각은 어느 실행의 관찰인지 설명하지만 최신성의 유일한 근거는 아닙니다. 최근 시각이라도 검사 뒤 파일이 바뀌면 다시 확인해야 합니다. 로컬 출력에는 변하는 시각을 그대로 예상 출력으로 박아 넣지 않고 자신의 state.json에서 확인합니다.
파일 목록은 검증 입력의 경계입니다
files는 명시한 상대 경로 배열입니다. 각 파일의 바이트를 읽어 SHA-256을 계산하며 파일이 없으면 해당 값은 null입니다. 빠진 입력 파일을 빈 문자열의 해시로 대신하면 실제 누락을 감출 수 있습니다. 큰 저장소에서 모든 파일을 무작정 읽기보다 검증에 영향을 주는 입력을 선정합니다. 이번 미션 regression에는 Java 소스·테스트·명세·pom.xml·Wrapper·참조 모델을 포함합니다. 파일 목록에서 의존 입력을 빼면 그 변경을 탐지할 수 없으므로 목록의 충분성은 사람이 검토합니다.
검사 전후의 바이트를 함께 비교합니다
검사 시작 직전에 beforeHashes를 수집하고 명령 종료 후 hashes를 다시 수집합니다. 종료 코드 0이어도 두 값이 다르면 status는 pending입니다. 검사 도중 코드가 바뀌었을 수 있어 어느 내용이 검증되었는지 보장하기 어렵기 때문입니다. result에는 자식 명령의 passed를 그대로 남기되 현재 완료 자격은 주지 않습니다. 두 스냅샷 사이에 변경 후 원상 복구된 경우까지 완전히 탐지하는 구현은 아닙니다. 단일 작성자로 입력을 유지한 채 검사하는 실습 전제를 지킵니다.
로그도 연결할 산출물입니다
표준 출력과 표준 오류를 합쳐 evidence/작업ID.log로 저장하고 그 파일의 logHash도 기록합니다. 합치는 방식은 두 스트림의 정확한 발생 순서까지 재현하지 않지만 오류 내용을 빠뜨리지 않습니다. 개인 제목이나 토큰을 실행 입력으로 넣지 않아 로그 수집 전에 노출을 줄입니다. 같은 작업을 다시 검사하면 이 작은 하네스는 최신 로그로 교체하므로 전체 시도 이력을 보관하는 시스템은 아닙니다. 과거 실패를 인계해야 한다면 비식별화한 요약과 별도 근거를 사람이 보존합니다.
상태 저장 도중 파일을 자르지 않습니다
save는 같은 폴더에 state.json.tmp를 먼저 작성한 뒤 replace로 state.json을 교체합니다. JSON을 기존 파일에 직접 쓰다가 중단되어 반쪽 상태가 남는 위험을 줄이는 방식입니다. 임시 파일 교체가 전원 손실 뒤 디스크 기록 유지나 두 실행기의 동시 갱신을 모두 해결하지는 않습니다. 여러 작업자가 같은 상태를 쓰지 않도록 이번 ZIP은 한 실행기에서 사용합니다. JSONDecodeError가 나오면 무작정 완료로 초기화하지 않고 손상 파일과 로그를 확인해 복구할 자료가 있는지 조사합니다.
실습 테스트의 실패를 정확히 읽습니다
bash check.sh를 실행하면 unittest의 test_failure_exit_is_preserved가 starter에서 실패합니다. 기대 7인데 실제 0이면 기록기가 실패를 성공으로 바꾼 것입니다. Java 명세나 제목 정렬을 고칠 문제는 아닙니다. solution의 테스트는 성공·실패·명령 없음·입력 누락·검사 중 파일 변경·로그 손상도 함께 검사합니다. fixture는 TemporaryDirectory 안에 만들어지고 실행이 끝나면 정리됩니다. 외부 API와 운영 설정을 읽지 않으므로 자신의 개인 할 일 데이터로 재현하지 않아도 됩니다.
미션에 적용할 때의 판단
lesson ZIP의 harness.py 수정 후 전체 테스트를 통과시키고 미션의 같은 계약에 적용합니다. 미션에서 python3 checkpoint.py rules를 먼저 실행한 다음 harness, regression을 실행하면 선행 검증과 기록 연결이 드러납니다. Java regression은 Surefire 보고서의 테스트 수·실패·오류·건너뜀을 확인하고 Python 참조 모델도 실행합니다. 단일 자식 명령의 종료 코드만 믿는 것보다 검사 정의를 구체화한 것입니다. 더 읽기의 맥락 장에서는 기록 선택을 넓게 다루며 여기서는 실행 관찰을 손실 없이 저장하는 데 집중합니다.
따라하기
실제 자식 종료 코드를 수집합니다
검사가 실패해도 기록기는 결과를 읽을 수 있습니다. 자식 명령의 관찰을 출력합니다.
import subprocess, sys
p = subprocess.run([sys.executable, '-c', 'print("ORDER comparison failed"); raise SystemExit(7)'], capture_output=True, text=True)
print(p.stdout.strip())
print('exitCode:', p.returncode)
print('result:', 'passed' if p.returncode == 0 else 'failed')실행 결과
ORDER comparison failed exitCode: 7 result: failed
바이트의 해시를 비교합니다
임시 폴더에 합성 파일을 만들고 순서만 바꿉니다. 길이가 같아도 해시는 달라집니다.
from pathlib import Path
from tempfile import TemporaryDirectory
import hashlib
with TemporaryDirectory() as folder:
path = Path(folder) / 'task.txt'
path.write_bytes(b'Z\nA\n')
before = hashlib.sha256(path.read_bytes()).hexdigest()
path.write_bytes(b'A\nZ\n')
after = hashlib.sha256(path.read_bytes()).hexdigest()
print('동일 입력:', before == after)
print('상태:', 'verified' if before == after else 'pending')실행 결과
동일 입력: False 상태: pending
기록기의 실패 검출을 확인합니다
ai-workflow-checkpoint starter를 풀어 그 폴더에서 실행합니다. test_failure_exit_is_preserved의 기대 7과 실제 0을 읽고 harness.py의 자식 종료 코드 수집을 수정합니다. 수정 뒤 14개 테스트가 모두 OK인지 확인합니다. 실제 출력은 자신의 실행에서 기록합니다.
bash check.sh확인 문제
실습
harness.py의 capture가 실제 자식 종료 코드를 저장하게 고칩니다. test_harness.py는 보존합니다. 성공·실패·미실행, 입력 전후 변화, 저장과 재개 계약까지 14개 검사를 실행합니다.
실행 명령
bash check.sh
기대 결과
starter는 자식 실패 종료 코드 검사에 실패합니다. 수정 및 solution은 14개 unittest 전부 OK이며 종료 코드 0입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 세션이 바뀌어도 작업을 이어갈 기록을 설명해 주시면 됩니다.