완료 상태 전이 검사
70분 안팎
학습 목표
미완료에서 완료로 바뀌는 전이와 완료 재요청의 결과를 모델로 검사합니다.
개념
값 하나보다 요청의 순서가 중요합니다
완료 기능의 핵심은 true를 반환하는 데서 끝나지 않습니다. 저장된 항목이 미완료에서 완료로 바뀌고 같은 요청을 다시 보내도 완료 상태를 유지해야 합니다. 첫 요청만 확인하면 true를 false로 되돌리는 토글 구현도 통과합니다. 이번 레슨에서는 작은 상태 전이 모델로 요청 순서를 기록하고 반환값과 최종 저장 상태를 함께 비교합니다. 서비스에 이미 있는 complete 계약을 모델로 설명한 뒤 다음 레슨의 Java 검사에 옮길 근거를 만듭니다.
상태와 사건을 구분합니다
상태는 미완료·완료·항목 없음이고 사건은 특정 번호에 대한 완료 요청입니다. 표의 행은 요청 전 상태, 요청, 응답, 요청 후 상태로 구성합니다. 미완료 항목의 완료는 성공과 완료 상태입니다. 완료 항목의 재완료도 성공과 완료 상태입니다. 항목 없음의 완료는 NOT_FOUND와 기존 상태 보존입니다. 저장된 항목이 없다는 상태를 새 빈 항목 생성으로 바꾸지 않는다는 점까지 적어야 누락 ID 처리 결함을 잡을 수 있습니다.
반복 성공이 뜻하는 것
같은 완료 요청을 두 번 보내면 두 응답의 id·title·done이 같아야 합니다. 이 작업의 반복은 새 생성이나 완료 취소가 아닙니다. 이를 멱등적인 결과라고 설명할 수 있습니다. 단, 지금 검사하는 것은 메모리 안의 업무 결과이며 네트워크 재시도와 동시 요청 처리를 입증한 것은 아닙니다. 별도의 완료 취소 기능을 원하면 경로와 요구를 새로 정의해야 합니다. 완료 버튼을 누를 때마다 상태를 반전시키는 편의 구현은 현재 계약에 맞지 않습니다.
전체 상태의 불변 조건
번호 1을 완료해도 번호 2의 제목과 done은 바뀌지 않습니다. 전체 목록의 순서와 개수도 유지됩니다. 완료 때문에 다음 생성 번호가 증가해서도 안 됩니다. 응답만 true로 만들고 저장소는 그대로 두는 구현을 검출하려면 완료 뒤 조회를 확인합니다. 목록 전체를 literal 기대값과 비교하면 다른 항목 손상도 발견할 수 있습니다. 특정 필드만 비교하는 짧은 검사는 편하지만 그 검사가 보호하지 않는 필드가 있다는 사실을 기록해야 합니다.
없는 번호와 잘못된 번호
없는 양의 정수 99는 NOT_FOUND입니다. 0·음수·문자열·boolean 같은 서비스 입력은 INVALID_ID입니다. Python에서 bool은 int와 일부 동작을 공유하므로 isinstance만으로 정수를 허용하면 True가 번호 1로 처리될 수 있습니다. 이 모델에서는 type(value) is int로 정수 입력을 구분합니다. HTTP 경로 문자열은 뒤 레슨에서 별도 파싱하므로 여기의 서비스 모델에 문자열 숫자를 억지로 허용하지 않습니다. 경계마다 받은 자료형의 뜻을 나눕니다.
모델에는 관찰할 정보만 둡니다
브라우저 입력은 tasks 배열과 ids 배열입니다. tasks에는 id·title·done이 있고 번호는 1부터 연속이며 중복되지 않는 정상 초기 상태를 제공합니다. ids는 순서대로 보낼 완료 요청입니다. 출력은 results와 tasks를 가진 JSON 객체입니다. results는 성공 시 ok와 task, 오류 시 ok와 error를 담습니다. 최종 tasks는 번호 순서로 내보냅니다. 저장소 구현이나 HTTP 상태를 흉내 내지 않고 이번 목표인 완료 순서와 상태 보존에 필요한 정보만 사용합니다.
같은 테스트 안에서 순서를 유지합니다
각 독립 테스트는 새 초기 상태로 시작합니다. 재완료 사례 하나 안에서는 두 요청이 같은 상태를 공유해야 하지만 서로 다른 테스트끼리는 목록을 공유하지 않습니다. 전역 배열을 재사용하면 단독 실행은 통과하고 전체 실행은 실패할 수 있습니다. 준비 단계에서 초기 항목을 복사하고 그 사례의 요청만 적용합니다. 이 구분은 상태 전이 검사에서 특히 중요합니다. 독립성이 필요한 경계와 연속성이 필요한 경계를 입력 구조로 명확히 드러냅니다.
스냅샷을 복사하는 이유
첫 완료 응답을 저장한 뒤 다음 요청이 같은 사전을 변경하면 과거 응답까지 바뀔 수 있습니다. 결과 기록에는 현재 항목의 복사본을 넣습니다. 최종 상태와 각 시점의 응답을 분리하면 첫 응답이 무엇이었는지 나중에도 확인할 수 있습니다. Java의 Task는 불변 record여서 새 completed 값을 만드는 구조입니다. Python 모델에서 가변 사전을 사용한다면 이 차이를 스스로 처리해야 합니다. 언어가 달라도 관찰 시점의 계약은 유지합니다.
오류 뒤의 관찰을 추가합니다
없는 번호를 요청한 다음 정상 번호를 완료하는 순서를 넣으면 오류가 흐름 전체를 중단시키는지도 확인할 수 있습니다. 오류 결과를 results에 남기고 다음 요청을 계속 처리합니다. 이 브라우저 모델은 여러 명령을 한 입력으로 검증하기 위해 그렇게 정의했습니다. 실제 HTTP에서는 각 요청이 독립 응답을 받습니다. 정상 번호 뒤에 없는 번호를 요청하는 반대 순서도 생각해 보고 상태 보존이라는 약속이 어느 시점에 검사되어야 하는지 설명합니다.
모델을 구현의 사본으로 만들지 않습니다
기대 JSON을 모델 출력에서 자동 복사하면 모델과 테스트가 같은 실수를 공유할 수 있습니다. 재완료는 true를 유지한다는 문장에서 두 성공 응답의 done=true를 직접 적습니다. 다른 항목은 바뀌지 않는다는 문장에서 번호 2의 done=false를 적습니다. 모델 코드는 이러한 기대 사례를 처리하는 학습 대상이지 기대값의 권위가 아닙니다. Java와 Python의 결과가 같아도 둘 다 토글한다면 요구에는 어긋납니다. 독립 사례가 비교의 기준을 담당합니다.
흔한 오류를 순서에서 찾습니다
두 번째 요청 뒤 False가 나오면 반전 연산을 사용했는지 확인합니다. 없는 ID에서 KeyError가 나면 존재 확인 전에 사전을 읽었는지 봅니다. 결과가 두 개여야 하는데 하나라면 오류 뒤 반복문이 종료되었는지 찾습니다. 입력에 []가 올 때는 결과도 빈 배열이어야 합니다. JSON 파싱 실패는 업무 실패 코드와 다르므로 입력 문법부터 확인합니다. 실패한 순서를 줄이더라도 첫 완료와 두 번째 완료라는 재현 조건은 지우지 않습니다.
전이 모델의 제출 기준
미완료 완료, 완료 재요청, 없는 번호, 잘못된 타입, 두 항목 중 하나만 완료, 요청 없음의 사례를 실행합니다. 결과 배열과 최종 목록을 모두 비교합니다. 모델이 통과한 뒤 어떤 전이가 확인되었고 어떤 입력이 아직 없었는지 표에 적습니다. 시간이나 난수가 없는 고정 입력을 사용하므로 다른 사람이 같은 결과를 얻을 수 있습니다. 상태 준비를 관리하는 일반 테스트 도구는 더 읽기로 보내고 이번에는 순서와 기대 상태를 직접 다루는 데 집중합니다.
따라하기
전이 표를 출력합니다
요청 전후 상태와 응답을 분리한 계획 표입니다. 실제 서비스 실행은 다음 로컬 레슨에서 확인합니다.
rows = [("미완료","성공","완료"),("완료","성공","완료"),("없음","NOT_FOUND","불변")]
for before, response, after in rows:
print(before, response, after)실행 결과
미완료 성공 완료 완료 성공 완료 없음 NOT_FOUND 불변
반전 구현을 구별합니다
토글이 첫 완료만 통과하는 이유를 재현합니다.
done = False
observed = []
for _ in range(2):
done = not done
observed.append(done)
print("토글 응답:", observed)
print("명세와 일치:", observed == [True, True])실행 결과
토글 응답: [True, False] 명세와 일치: False
타입 경계를 확인합니다
이 코드는 양의 정수 형식만 판정합니다. 존재 여부는 따로 검사합니다.
for value in [1, True, "1", 0]:
valid = type(value) is int and value > 0
print(repr(value), valid)실행 결과
1 True True False '1' False 0 False
확인 문제
실습
입력은 tasks와 ids를 가진 JSON 객체입니다. tasks는 1부터 연속된 고유 번호의 정상 항목 배열이며 title은 문자열, done은 boolean입니다. ids 순서대로 완료를 처리합니다. 양의 정수 아닌 값은 INVALID_ID, 없는 양의 정수는 NOT_FOUND입니다. 성공 결과는 ok=true와 task, 실패는 ok=false와 error입니다. results 배열과 번호 순서의 최종 tasks 배열을 가진 JSON 한 줄을 출력합니다. 응답 항목을 복사해 각 시점의 값을 보존합니다.
모범 답안
import json, sys
payload = json.load(sys.stdin)
tasks = {t["id"]: dict(t) for t in payload["tasks"]}
results = []
for value in payload["ids"]:
if type(value) is not int or value <= 0:
results.append({"ok": False, "error": "INVALID_ID"})
elif value not in tasks:
results.append({"ok": False, "error": "NOT_FOUND"})
else:
tasks[value]["done"] = True
results.append({"ok": True, "task": dict(tasks[value])})
print(json.dumps({"results": results, "tasks": [tasks[k] for k in sorted(tasks)]}, ensure_ascii=False, separators=(",", ":")))
더 읽기
면접 질문
- 구현과 같은 실수를 반복하지 않는 테스트를 만드는 방법을 설명해 주시면 됩니다.