작업 목록과 상태 계약
90분 안팎
학습 목표
작업 ID·의존성·대기 및 진행 및 검증완료 상태를 Python으로 검증합니다.
개념
배열 순번으로 상태를 저장하면 주인이 바뀝니다
새 검사를 작업 목록 가운데 넣으면 기존 세 번째 작업이 네 번째로 이동합니다. 상태를 순번으로 저장했다면 완료 표시가 다른 작업에 붙을 수 있습니다. 그래서 할 일 앱 하네스는 rules·harness·regression처럼 의미가 있는 작업 ID로 계획과 기록을 연결합니다. 앱의 할 일 ID와 하네스의 작업 ID는 별개입니다. 전자는 사용자가 생성한 항목을, 후자는 개발자가 수행할 검증 단계를 가리킵니다. 작업 이름은 사람이 읽을 수 있게 쓰되 완료 여부는 이름이 아니라 검증 계약으로 판단합니다.
계획과 관찰의 저장 위치
미션의 tasks.json은 version과 tasks 배열을 가진 계획입니다. 각 작업은 id·deps·check·files를 가집니다. deps는 선행 검증 ID, check는 등록된 검사 이름, files는 결과에 영향을 주는 파일 목록입니다. state.json은 planVersion과 tasks 객체를 가진 관찰 기록이며 객체 키는 같은 작업 ID입니다. 계획 파일에 현재 통과를 적고 관찰 파일에도 적으면 어느 값이 최신인지 혼동됩니다. 이번 브라우저 과제에서는 구조 검증을 쉽게 연습하도록 id·deps·status만 가진 합성 배열을 사용하고 실제 미션의 파일 분리 방식과 구별합니다.
세 상태의 판단 기준
pending은 아직 현재 근거로 검증되지 않은 상태, running은 검사 직전에 저장한 진행 상태, verified는 해당 정의와 파일에서 검사 통과 근거가 있는 상태입니다. 상태 전이는 pending에서 running, 검사 완료 뒤 verified 또는 pending으로 이동합니다. 실패 결과 자체는 result에 failed로 보존하므로 pending이 모든 과거 관찰을 지우는 뜻은 아닙니다. 브라우저 계약은 상태 이름과 의존성 일관성만 검사합니다. verified라는 문자열이 실제 명령 실행을 증명하는지는 다음 체크포인트 레슨에서 보완합니다.
입력 계약을 먼저 고정합니다
브라우저 입력은 JSON 배열 하나이며 각 원소는 정확히 id·deps·status 세 필드를 가진 객체입니다. id는 공백만 있지 않은 문자열, deps는 문자열 배열입니다. id 중복은 허용하지 않으며 status는 pending·running·verified 중 하나입니다. 빈 배열은 아직 계획이 없는 정상 상태로 OK를 반환합니다. 잘못된 JSON 자체는 채점 입력에서 제공하지 않지만 파싱 오류가 발생하면 구조 검사 이전 단계라는 점을 이해합니다. 문자열 대신 숫자 ID를 받아 자동 변환하는 편의 기능은 이 계약에 포함하지 않습니다.
검사 순서가 오류 코드의 의미를 만듭니다
형식이 맞지 않으면 SHAPE, 중복 ID면 DUPLICATE, 없는 의존성이면 MISSING, 순환이면 CYCLE, 상태 이름이 틀리면 STATE, 선행 상태가 준비되지 않았으면 BLOCKED를 출력합니다. 여러 잘못이 섞인 입력은 이 순서에서 먼저 발견한 오류 하나를 반환합니다. 채점에서 기대와 다른 코드가 나오면 출력 철자뿐 아니라 검사 순서를 확인합니다. 존재하지 않는 작업을 그래프로 탐색하면 KeyError가 먼저 나와 의도한 MISSING을 잃기 때문에 존재 검사를 그래프 탐색 앞에 둡니다.
딕셔너리로 바꾸기 전에 중복을 잡습니다
작업 배열을 곧바로 id를 키로 하는 딕셔너리로 바꾸면 같은 ID 두 항목 중 뒤의 항목만 남습니다. 잘못된 계획이 정상 한 항목처럼 보이는 문제가 생깁니다. 먼저 ID 목록을 만들고 목록 길이와 집합 길이를 비교합니다. 다를 때 DUPLICATE를 반환한 다음에만 그래프를 만듭니다. 중복 작업을 자동 합치면 각 작업의 검증 의미와 상태가 충돌할 수 있으므로 교육 과제에서는 거절합니다. 중복 deps는 같은 선행 조건을 반복할 뿐이며 이 작은 계약에서는 허용합니다.
의존성은 목록 위치와 관계없습니다
regression이 harness에 의존한다면 deps에 harness를 명시합니다. harness가 배열 뒤에 있어도 존재하는 ID라면 참조할 수 있습니다. 배열 순서를 실행 순서라고 가정하지 않고 관계를 따로 검사합니다. 완료한 선행 작업을 기준으로 실행 후보를 고르므로 표시 순서를 바꾸어도 논리 관계는 유지되어야 합니다. 브라우저 과제는 실행 후보를 출력하지 않고 관계가 유효한지만 확인합니다. 실행 가능한 순서를 만드는 작업과 그래프 자체의 유효성을 판단하는 작업을 섞지 않습니다.
순환은 완료를 기다릴 수 없는 약속입니다
a가 b를 기다리고 b가 a를 기다리면 어느 쪽도 먼저 시작할 수 없습니다. 자기 자신을 deps에 적은 한 작업도 순환입니다. 깊이 우선 탐색에서 지금 방문 중인 ID를 visiting에 넣고, 그 ID를 다시 만나면 CYCLE을 냅니다. 탐색을 마친 ID는 done에 넣어 재탐색을 줄입니다. 방문한 적이 있다는 이유만으로 순환이라고 판단하면 두 작업이 공통 선행 작업을 참조하는 정상 그래프도 거절하게 됩니다. visiting과 done의 서로 다른 의미를 손으로 그림을 그려 확인합니다.
진행 상태에도 선행 조건을 적용합니다
running 또는 verified인 작업은 모든 직접 의존 작업이 verified여야 합니다. rules가 pending인데 harness가 verified이면 BLOCKED입니다. 아직 pending인 작업은 선행이 pending이어도 유효한 계획입니다. 대기 상태가 있다는 이유로 전체 파일을 오류 처리하면 중간 진행을 저장할 수 없습니다. 상태 이름은 먼저 검사하므로 done처럼 지원하지 않는 값은 BLOCKED가 아니라 STATE로 보고합니다. 코드가 유연하게 받아들인다고 좋은 계약이 되는 것은 아닙니다. 의미가 정의된 상태만 처리합니다.
흔한 구현 오류를 예상합니다
모든 상태가 verified인지 확인하는 구현은 정상 대기 계획을 거절합니다. ID를 set으로 바꾼 뒤 개수를 세면 중복을 놓칩니다. 순환을 한 번의 인접 비교로 판단하면 세 작업 이상의 고리를 놓칩니다. RecursionError가 발생하면 visiting에서 재방문을 탐지하는 코드가 빠졌는지 봅니다. KeyError는 없는 deps를 탐색했거나 필수 키 검사 전에 접근했다는 신호입니다. 이런 오류는 완료 상태의 증거 부족 문제와 다르므로 입력 구조와 탐색 단계부터 좁혀 조사합니다.
경계 사례로 검사기의 쓸모를 확인합니다
테스트에는 빈 계획, 독립 대기 작업, 앞선 verified와 뒤따르는 running, 중복 ID, 없는 의존성, 자기 고리, 여러 작업의 고리, 잘못된 상태, 준비 안 된 선행을 둡니다. 정상 자료만 통과하면 잘못된 계획을 거절하는 능력을 알 수 없습니다. 순환과 BLOCKED가 동시에 성립하는 입력도 순환이 먼저 보고되는지 확인합니다. 성공 출력은 OK 하나이고 디버깅용 ID 목록을 같은 표준 출력에 추가하지 않습니다. 채점 계약과 관찰용 출력은 분리합니다.
구조 검사와 완료 증명 사이
이번 함수가 OK를 출력하면 정의된 형식과 관계가 일관된다는 뜻입니다. 검증 명령이 실제로 수행되었다거나 앱의 생성·완료 요구가 충족되었다는 뜻으로 확대하지 않습니다. 사람은 각 작업의 check와 files가 요구에 맞는지도 검토합니다. 다음 레슨은 같은 ID의 실행 결과를 시각·종료 코드·해시와 연결하고 재개 레슨은 그 근거가 아직 유효한지 확인합니다. 더 읽기의 계획 장에서 다른 작업 흐름을 살펴보되 브라우저 제출은 이 오류 우선순위와 상태 계약을 구현합니다.
따라하기
중복 ID를 먼저 관찰합니다
매핑으로 변환하기 전에 중복을 찾습니다.
ids = ['rules', 'harness', 'rules']
print('배열 길이:', len(ids))
print('고유 ID:', len(set(ids)))
print('중복:', len(ids) != len(set(ids)))실행 결과
배열 길이: 3 고유 ID: 2 중복: True
선행 상태를 읽습니다
뒤쪽 ID를 참조해도 존재할 수 있습니다. 진행 상태에 선행 조건을 적용합니다.
tasks = [{'id':'harness','deps':['rules'],'status':'running'}, {'id':'rules','deps':[],'status':'pending'}]
status = {t['id']: t['status'] for t in tasks}
for t in tasks:
blocked = t['status'] in ('running', 'verified') and any(status[d] != 'verified' for d in t['deps'])
print(t['id'], 'BLOCKED' if blocked else '유효')실행 결과
harness BLOCKED rules 유효
한 고리를 손으로 따라갑니다
이 코드는 각 ID에 선행 하나인 고정 사례만 관찰합니다. 브라우저 제출은 여러 의존성을 탐색하는 일반 함수를 구현합니다.
graph = {'a':['b'], 'b':['c'], 'c':['a']}
path = []
key = 'a'
while key not in path:
path.append(key)
key = graph[key][0]
print('경로:', ' -> '.join(path + [key]))
print('다시 만난 ID:', key)실행 결과
경로: a -> b -> c -> a 다시 만난 ID: a
확인 문제
실습
JSON 작업 배열을 읽고 형식→중복 ID→없는 의존성→순환→상태 이름→선행 상태 순으로 검사해 첫 오류 코드 하나 또는 OK를 출력합니다. 각 항목은 정확히 id·deps·status이며 비어 있지 않은 문자열 ID, 문자열 deps 배열, pending·running·verified 상태를 가집니다. running·verified의 직접 선행은 모두 verified여야 합니다. 빈 계획은 OK, 자기 의존은 CYCLE입니다. 잘못된 JSON 문법은 입력 범위 밖입니다.
모범 답안
import json, sys
def validate_tasks(tasks):
if not isinstance(tasks, list):
raise ValueError('SHAPE')
ids = []
for t in tasks:
if not isinstance(t, dict) or set(t) != {'id', 'deps', 'status'}:
raise ValueError('SHAPE')
if not isinstance(t['id'], str) or not t['id'].strip():
raise ValueError('SHAPE')
if not isinstance(t['deps'], list) or any(not isinstance(d, str) for d in t['deps']):
raise ValueError('SHAPE')
ids.append(t['id'])
if len(ids) != len(set(ids)):
raise ValueError('DUPLICATE')
if any(d not in ids for t in tasks for d in t['deps']):
raise ValueError('MISSING')
graph = {t['id']: t['deps'] for t in tasks}
visiting, done = set(), set()
def visit(key):
if key in visiting:
raise ValueError('CYCLE')
if key in done:
return
visiting.add(key)
for d in graph[key]:
visit(d)
visiting.remove(key)
done.add(key)
for key in graph:
visit(key)
if any(t['status'] not in ('pending', 'running', 'verified') for t in tasks):
raise ValueError('STATE')
statuses = {t['id']: t['status'] for t in tasks}
if any(t['status'] in ('running', 'verified') and any(statuses[d] != 'verified' for d in t['deps']) for t in tasks):
raise ValueError('BLOCKED')
return 'OK'
try:
print(validate_tasks(json.load(sys.stdin)))
except ValueError as error:
print(str(error))
더 읽기
면접 질문
- 세션이 바뀌어도 작업을 이어갈 기록을 설명해 주시면 됩니다.