재현 가능한 오류 보고
75분 안팎
학습 목표
기대 결과·실제 결과·재현 절차를 구분합니다.
개념
왜 재현 절차가 먼저인가
금액 입력이 안 된다는 말만으로는 공백인지 abc인지 음수인지 알 수 없습니다. 보고서는 담당자가 같은 상태와 입력으로 같은 현상을 얻도록 돕습니다. 독서 기록 추가 중 abc를 넣었을 때 기록이 늘어나는 사례를 다룹니다. 증상을 관찰한 뒤 기대 결과를 계약에 연결하고 원인은 호출 경로와 코드로 확인합니다. 추측을 원인으로 확정해 적지 않습니다. 이번 목표는 기대·실제·재현·수정 근거를 구분해 docs/bugs.md에 남기는 것입니다.
초기 상태와 정확한 입력
기존 기록은 제목 달빛, pages 10, cost 12000, completed 1인 한 건입니다. 새 제목 별빛, 쪽수 1, 금액 abc, 완료 0을 추가합니다. 기대는 ERR_COST_NOT_INTEGER이며 기존 목록 전체가 같고 총금액은 12000이어야 합니다. 실제는 반환 오류 코드와 호출 후 목록을 함께 적습니다. 숫자 아닌 금액이 오류로 표시되어도 목록이 늘면 데이터 보존 계약은 깨졌습니다. 앞뒤 공백이나 빈 줄이 원인에 중요하면 repr로 표시해 다른 사람이 입력을 그대로 만들게 합니다.
실행 조건을 최소한으로 기록
압축을 풀어 README 명령을 실행한 폴더, python3 --version의 실제 버전, 검사한 파일 이름과 함수를 기록합니다. 운영 데이터나 실제 독자 이름은 사용하지 않고 예제 한 건으로 줄입니다. 미션 함수는 파일을 저장하지 않으므로 같은 Python 프로세스의 목록을 비교하는 재현입니다. CLI 한 건 검사와 영구 저장 문제로 표현하지 않습니다. 입력을 한꺼번에 바꾸지 않고 정상 상태를 만든 다음 금액 한 항목만 바꿔야 원인을 좁힐 수 있습니다.
수정 전후에 같은 질문을 던집니다
수정 전에는 기록을 먼저 append해서 뒤늦은 금액 실패가 목록을 남깁니다. 수정 후에는 제목·쪽수·금액·완료 표시 검사를 모두 통과한 뒤 append합니다. 예외를 잡아 0으로 저장하는 방식은 계약을 바꿉니다. 동일한 입력으로 반환 코드와 목록 전체를 비교하고 기존 20개 회귀 검사도 실행합니다. 자동 문서 검사는 항목의 존재만 볼 수 있으므로 사람이 보고 초기 상태와 실제 관찰이 코드에 맞는지 읽어야 합니다. 실행하지 않은 검사에는 미확인이라고 적습니다.
읽기 쉬운 보고서의 구조
제목은 금액 abc 거절 뒤 기록 수 증가처럼 입력과 증상을 담습니다. 재현 단계에는 준비 상태, 호출, 관찰 순서를 씁니다. 기대 결과에는 요구 근거, 실제 결과에는 관찰한 값, 호출 위치에는 파일과 함수, 원인에는 확인한 변경 순서를 적습니다. 수정에는 바꾼 책임과 이유를 설명하고 검증에는 명령·실행 수·실패 수를 남깁니다. 해결 완료는 확인한 범위를 뜻하며 모든 오류가 없다는 말로 확대하지 않습니다. 수정 내용을 다음 담당자가 되풀이해서 확인할 수 있어야 합니다.
관찰 자료를 표로 정리하기
수정 전과 수정 후에 초기 목록, 추가 입력, 반환 코드, 호출 후 건수, 총금액을 같은 항목으로 기록합니다. 금액 abc를 거절했는데 건수가 2가 된 경우에는 오류 코드가 맞다는 사실과 실패 기록이 남았다는 사실을 동시에 씁니다. 합계가 12000으로 같더라도 새 기록의 금액이 0이라면 목록 손상을 숨길 수 있습니다. 그래서 전체 목록 보존 여부도 별도의 관찰 항목입니다. 보고서에 적은 실제 값은 코드를 읽고 예상한 값이 아니라 같은 초기 상태에서 실행해 얻은 값이어야 합니다.
복사해서 실행할 수 있는 재현
호출 예시에는 records를 만드는 줄과 add_record에 전달하는 모든 인자를 포함합니다. 금액만 abc라고 적고 쪽수와 완료 표시를 생략하면 다른 담당자가 앞선 검증에서 멈출 수 있습니다. import에 필요한 파일과 실행 위치도 적어 같은 함수가 선택되게 합니다. 목록을 수정 전 실험에서 그대로 재사용하면 수정 후에도 이미 손상된 상태에서 시작할 수 있으므로 매 실험 전에 초기 한 건을 다시 만듭니다. 이 준비 과정까지 재현 단계에 포함해야 결과 차이를 구현 변경으로 설명할 수 있습니다.
가설을 확인된 원인으로 바꾸기
처음에는 금액 변환 전에 append가 실행되는지 의심할 수 있습니다. 실제 records.py에서 추가 줄이 검증보다 앞선 것을 확인하고 실패 호출 뒤 새 항목을 관찰하면 그 순서를 원인으로 기록할 근거가 생깁니다. Python 자체 오류나 데이터베이스 장애라는 설명은 이번 재현에 근거가 없습니다. 예외 메시지가 ValueError라고 해서 모든 ValueError가 같은 결함인 것도 아닙니다. 어느 입력이 어떤 줄에 도달했고 실패 이전에 어떤 변경을 남겼는지를 연결해야 수정할 책임이 분명해집니다.
수정 설명과 검증 설명을 나누기
수정란에는 모든 필드 검증이 끝난 뒤 기록을 추가하도록 순서를 바꾸었다고 적습니다. 검증란에는 abc 입력에서 오류 코드를 유지하면서 건수 1, 기존 목록 보존, 총금액 12000을 확인했다고 적습니다. 두 설명을 나누면 의도와 관찰이 일치하는지 검토할 수 있습니다. 정상 입력에서는 한 건이 추가되는지도 확인해야 실패를 막기 위해 모든 추가를 제거한 구현을 걸러낼 수 있습니다. 회귀 검사는 기존 정상 동작을 확인한 명령과 실행 수를 남기며 한 번의 실패 재현만으로 전체 기능 검증을 대신하지 않습니다.
자동 문서 검사의 한계
docs/bugs.md에 기대 결과나 수정 원인이라는 제목이 있어도 내용이 실제 코드와 다르면 보고서는 도움이 되지 않습니다. 자동 검사는 항목 누락을 찾는 보조 수단이며 입력값과 숫자의 정확성은 실행 자료와 대조합니다. 테스트 결과를 붙일 때는 실패한 실행과 통과한 실행이 어느 구현에서 나온 것인지 표시합니다. 로그 전체를 붙여 독자가 핵심 값을 찾게 하기보다 관련 반환값과 상태를 본문에 정리하고 필요한 실행 근거를 덧붙입니다. 파일 이름만 적은 보고서보다 호출 코드와 관찰값이 있는 보고서가 재현에 유용합니다.
해결 범위와 남은 확인을 전달하기
보고서의 마지막에는 확인한 입력과 아직 확인하지 않은 조건을 구분해 적습니다. 이번 함수는 메모리 목록을 바꾸므로 파일 저장의 원자성이나 여러 사용자 동시 입력까지 해결했다고 쓰지 않습니다. 테스트를 실행할 수 없었다면 이유와 미확인 항목을 남기고 통과 수를 만들어 적지 않습니다. 수정 후 재현이 사라졌다는 사실은 같은 초기 상태와 입력에서 확인되어야 합니다. 다음 담당자는 문서의 절차를 따라 실패 보존과 정상 추가를 다시 확인할 수 있어야 하며, 그 결과가 다르면 실행 버전과 준비 상태부터 비교합니다.
따라하기
수정 전 상태 손상
별도 demo.py로 실행합니다. 이 실험은 미션 starter의 먼저 추가하는 순서가 왜 잘못인지 보여 줍니다.
records = [dict(title="달빛", pages=10, cost=12000, completed=1)]
before = [dict(record) for record in records]
records.append(dict(title="별빛", pages=1, cost=0, completed=0))
try:
records[-1]["cost"] = int("abc")
except ValueError:
print("ERR_COST_NOT_INTEGER")
print("count", len(records))
print("preserved", records == before)
실행 결과
ERR_COST_NOT_INTEGER count 2 preserved False
금액 검증 후 변경
별도 demo.py로 실행합니다. 실제 미션에서는 금액뿐 아니라 다른 필드도 모두 검증한 뒤 추가합니다.
import re
def parse_number(raw):
text = raw.strip()
if not text:
return None, "EMPTY"
if not re.fullmatch(r"[+-]?[0-9]+", text):
return None, "NOT_INTEGER"
try:
value = int(text)
except ValueError:
return None, "NOT_INTEGER"
if value < 0:
return None, "NEGATIVE"
return value, None
records = [dict(title="달빛", pages=10, cost=12000, completed=1)]
before = [dict(record) for record in records]
cost, error = parse_number("abc")
if error is None:
records.append(dict(title="별빛", pages=1, cost=cost, completed=0))
else:
print("ERR_COST_" + error)
print("count", len(records))
print("preserved", records == before)
실행 결과
ERR_COST_NOT_INTEGER count 1 preserved True
보고서용 재현 입력 표현
다음 코드를 별도 파일 demo.py에 저장하고 python3 demo.py로 실행합니다.
print(repr("abc"))
print(repr(" "))
print("expected: count=1, cost=12000")
실행 결과
'abc' ' ' expected: count=1, cost=12000
확인 문제
실습
미션 starter와 solution에 같은 재현 입력을 실행하고 docs/bugs.md를 작성합니다. 제목, 환경(실제 Python 버전과 실행 폴더), 초기 한 건, 정확한 호출 코드, 기대 결과, 실제 반환 코드·건수·목록 보존 여부·합계, 호출 위치(records.py의 add_record와 parsing.py의 parse_cost), 확인한 원인, 수정 방식, 실행한 테스트 명령과 결과를 적습니다. 실행하지 않은 항목은 미확인이라고 씁니다. 합격 기준은 다른 사람이 문서만으로 같은 실패를 재현하고 같은 입력의 수정 후 보존을 확인하는 것입니다. 문서 자동 검사는 항목 존재만 확인하므로 값과 설명은 직접 대조합니다.
더 읽기
면접 질문
- 오류 보고서에서 기대 결과와 실제 결과를 어떻게 구분하나요?
- 실패 반환 뒤 데이터가 변경되는 버그를 어떻게 재현하고 검증하나요?