Devin.KR

상태 전이표 실행하기

75분 안팎

학습 목표

상태·이벤트 입력을 전이표로 처리하여 다음 상태와 동작을 출력합니다.

개념

상태·이벤트·동작을 분리합니다

센서가 빠졌는데 화면에는 마지막 온도가 계속 보이는 상황을 생각합니다. 숫자가 남아 있다는 사실과 지금 측정할 수 있다는 사실은 다릅니다. 통신 함수의 오류 반환만 추가하면 호출자가 이전 숫자를 다시 보낼 수 있습니다. 이번 모듈에서는 다음에 허용되는 작업을 상태 하나로 정하고, 오류가 들어오면 측정 결과의 유효성을 함께 바꿉니다. 상태 전이표를 실행하여 정상 경로와 금지 경로를 먼저 확인합니다.

공통 실행 환경은 Python 3 브라우저와 C11 로컬 실습입니다. 외부 패키지나 실물 장치는 사용하지 않습니다. 다운로드한 starter를 독립 폴더에 풀고 Makefile이 있는 위치에서 make test를 실행합니다. gcc 명령이 맥에서는 Apple Clang일 수 있으므로 gcc --version 결과를 실행 기록에 적습니다. 명령을 찾을 수 없다는 오류는 환경 준비 문제이며 FAIL 조건식은 구현 검증 실패입니다. 압축의 solution은 비교용 완성본입니다.

상태는 지금 어떤 작업 단계에 있는지를 나타냅니다. IDLE은 다음 수집을 기다리고 MEASURE는 센서 요청을 기다리며 TRANSMIT은 확정된 샘플을 공개할 준비입니다. RECOVER는 잠시 멈추어 복구 조건을 기다리고 ERROR는 자동 시도를 중단한 상태입니다. recording은 사용자의 기록 의도라서 별도입니다. 사용자가 기록을 켰어도 ERROR에서는 샘플을 만들지 않습니다. 상태와 사용자 설정을 같은 변수로 합치지 않습니다.

이벤트는 상태를 바꾸도록 들어온 사실입니다. START는 수집 시작, SAMPLE은 성공한 새 샘플, SENT는 공개 완료, TIMEOUT은 요청 제한 초과입니다. DETACH는 fixture가 알려 준 분리이고 RECONNECT는 명시적으로 복구를 승인한 입력입니다. STOP은 정지 요청, LIMIT는 총 시도 예산 소진입니다. 이벤트 이름만으로 동작이 결정되지 않습니다. ERROR에서 START가 와도 다시 읽지 않는 것처럼 현재 상태와 함께 판단합니다.

동작은 전이 때 수행할 효과입니다. READ는 읽기 시작, SEND는 전송 준비, WAIT는 다음 주기 대기, INVALIDATE는 현재 샘플을 무효화하는 뜻입니다. CLEAR는 대기 이벤트와 필터 창을 초기화하고 CANCEL은 진행 중 작업을 취소합니다. ALARM은 오류 표시를 요청하고 IGNORE는 효과 없이 현재 상태를 유지합니다. 출력 문자열은 Python 연습의 계약이며 실제 GPIO나 버스를 호출하지 않습니다. SEND가 나왔다고 UART 파형을 보냈다고 기록하지 않습니다.

핵심 전이는 IDLE/START→MEASURE/READ, MEASURE/SAMPLE→TRANSMIT/SEND, TRANSMIT/SENT→IDLE/WAIT입니다. 정상 순서를 먼저 적으면 읽기 전에 전송하는 오류를 찾기 쉽습니다. MEASURE/TIMEOUT은 RECOVER/INVALIDATE, MEASURE/LIMIT는 ERROR/ALARM입니다. 여기서는 재시도 횟수 계산을 입력 이벤트의 책임으로 둡니다. 다음 C 레슨이 실제 경과 시간과 예산을 판단하여 이 흐름을 구현합니다.

분리 이벤트는 ERROR를 제외한 모든 상태에서 RECOVER/INVALIDATE로 갑니다. 전송을 준비한 뒤 분리가 관찰되어도 공개를 막으려는 정책입니다. ERROR는 경보를 유지하고 DETACH를 무시합니다. RECONNECT는 RECOVER 또는 ERROR에서만 IDLE/CLEAR로 가며, 이때 바로 SEND하지 않습니다. 새 START와 새 SAMPLE을 거쳐야 정상 공개가 가능합니다. 케이블이 다시 보였다는 사실만으로 마지막 숫자를 새 측정으로 취급하지 않습니다.

STOP은 IDLE·MEASURE·TRANSMIT에서 IDLE/CANCEL로 처리합니다. RECOVER의 STOP은 IGNORE로 두어 복구 대기를 유지하고 ERROR의 STOP도 경보를 해제하지 않습니다. 사용자가 껐다 켜는 것만으로 실패 한도가 사라지면 불안정한 장치를 계속 두드릴 수 있습니다. 복구 입력의 의미를 분리해 전이표에 적습니다. 실제 미션은 분리 대기와 시간 초과 휴지를 구별하므로 단순 Python 표보다 조건을 더 갖습니다.

알려진 상태와 이벤트이지만 표에 없는 조합은 현재 상태와 IGNORE를 출력합니다. IDLE/SAMPLE은 유효한 요청을 시작하지 않았으므로 무시합니다. 반면 UNKNOWN/START나 IDLE/BOOT처럼 허용 목록 밖 입력은 INVALID 한 줄입니다. 입력 형식 오류와 정상적으로 무시할 이벤트를 구분해야 로그를 읽을 수 있습니다. 모든 빈칸을 ERROR 상태로 보내면 무해한 중복 이벤트도 고장으로 오인할 수 있습니다.

표준 입력은 공백으로 구분한 상태와 이벤트 두 토큰입니다. 한 번 실행할 때 한 조합만 처리하며 출력도 다음 상태와 동작 두 토큰입니다. 대문자 이름을 그대로 쓰고 세 번째 토큰이나 빈 입력은 INVALID입니다. split으로 읽은 뒤 길이를 검사하고 허용 집합을 확인한 후 사전을 조회합니다. 길이 검사 전에 v[1]을 읽어 IndexError가 나면 이벤트 로직이 아니라 입력 경계부터 고칩니다.

구현 순서는 상태 집합과 이벤트 집합, 명시 전이 사전, 공통 DETACH·STOP 규칙, 기본 IGNORE입니다. 공통 규칙과 개별 행의 우선순위가 겹치지 않는지 봅니다. ERROR에서 DETACH를 처리하는 예외가 빠지면 경보가 복구 대기로 덮입니다. 사전의 키는 두 문자열 튜플이고 값도 다음 상태·동작 튜플입니다. 문자열 키 하나에 상태와 이벤트를 이어 붙이면 오타를 찾기 어려워집니다.

확인할 불변조건은 ERROR에서 START로 읽지 않음, 분리 시 SEND하지 않음, 복구 직후 SAMPLE을 재사용하지 않음입니다. 정상 경로 세 줄만 통과시키면 이 조건들은 검사되지 않습니다. 시험에는 전송 중 분리, 오류 중 시작, 복구 중 정지, 알 수 없는 입력을 포함합니다. 각 기대 결과를 전이표에서 먼저 정하고 구현 실행과 대조합니다. 구현 결과를 그대로 기대값으로 복사하면 같은 실수가 숨어 있을 수 있습니다.

starter는 모든 정상 조합에 현재 상태와 IGNORE만 반환합니다. ERROR/START 같은 테스트가 통과하더라도 정상 전이는 아직 빠져 있습니다. 처음에는 IDLE/START와 MEASURE/SAMPLE 두 실패를 고치고 분리 규칙과 복구 행을 추가합니다. 출력이 INVALID라면 토큰 개수와 허용 집합부터 확인합니다. 다음 상태는 맞는데 동작이 다르면 사전의 값 또는 공통 규칙이 덮어쓰는 부분을 확인합니다.

전이표는 리뷰에서 구현과 요구를 연결하는 도구입니다. 상태·입력·조건·다음 상태·효과 다섯 열을 적으면 시간과 횟수 같은 보호 조건을 뒤에서 덧붙일 수 있습니다. Python 실습에는 조건이 없지만 C 미션에는 응답 도착, 경과 20ms, 총 세 번, 분리 해제 등이 들어갑니다. 그림을 예쁘게 만드는 것보다 빈 조합을 어떻게 처리할지 설명하는 일이 중요합니다. 표의 버전을 코드와 함께 남깁니다.

PC 전이 출력은 물리적인 센서 분리를 탐지했다는 증거가 아닙니다. DETACH는 우리가 주입한 이벤트이며 실물에서 NACK의 원인이 주소 오류인지 분리인지 추가 확인이 필요합니다. 이번 제출은 알려진 이벤트에 따른 동작을 재현하는 범위입니다. 서재 종합 프로젝트는 다른 장치의 상태 설계를 더 읽는 연결로 사용합니다. 해당 코드를 옮기기보다 우리 기록기의 오류 경로를 직접 정의합니다.

완료할 때는 IDLE START가 MEASURE READ로 바뀌는 이유와 ERROR START가 ERROR IGNORE인 이유를 말로 설명합니다. 결과 문자열만 외우면 새로운 이벤트가 추가될 때 정책을 정하기 어렵습니다. 상태가 늘어나도 동일한 검토 순서를 적용합니다. 가능한 정상 흐름을 먼저 적고, 실패 때 취소할 효과를 찾은 다음, 복구 후 다시 얻어야 할 데이터를 정합니다. 이 표가 다음 레슨의 요청 예산과 마지막 미션의 공개 조건을 연결합니다.

따라하기

정상 경로 세 조합

입력 조합을 한 번씩 독립 실행한 다음 상태와 동작입니다. 아래 코드에는 명세의 정상 행만 둡니다.

table = {("IDLE", "START"): ("MEASURE", "READ"), ("MEASURE", "SAMPLE"): ("TRANSMIT", "SEND"), ("TRANSMIT", "SENT"): ("IDLE", "WAIT")}
for key in table:
    print(*key, "=>", *table[key])

실행 결과

IDLE START => MEASURE READ
MEASURE SAMPLE => TRANSMIT SEND
TRANSMIT SENT => IDLE WAIT

오류 상태의 시작 요청

이미 알려진 조합이지만 전이표에 없는 이벤트는 무시합니다.

state, event = "ERROR", "START"
table = {}
print(*table.get((state, event), (state, "IGNORE")))

실행 결과

ERROR IGNORE

입력 형식 경계

길이가 두 토큰인지를 먼저 검사합니다. 다음 실습에서 허용 집합 검사도 추가합니다.

for text in ("IDLE START", "IDLE START extra", ""):
    values = text.split()
    print("PAIR" if len(values) == 2 else "INVALID")

실행 결과

PAIR
INVALID
INVALID

확인 문제

실습

상태 이벤트 두 토큰을 읽어 다음 상태 동작을 출력합니다. 본문의 전이표·공통 DETACH/STOP 규칙을 구현합니다. 알려진 미정의 조합은 현재 상태 IGNORE, 형식·허용 목록 오류는 INVALID입니다. 한 실행에 조합 하나입니다.

모범 답안
import sys
STATES = {"IDLE", "MEASURE", "TRANSMIT", "RECOVER", "ERROR"}
EVENTS = {"START", "SAMPLE", "SENT", "TIMEOUT", "DETACH", "RECONNECT", "STOP", "LIMIT"}
TABLE = {
    ("IDLE", "START"): ("MEASURE", "READ"),
    ("MEASURE", "SAMPLE"): ("TRANSMIT", "SEND"),
    ("TRANSMIT", "SENT"): ("IDLE", "WAIT"),
    ("MEASURE", "TIMEOUT"): ("RECOVER", "INVALIDATE"),
    ("MEASURE", "LIMIT"): ("ERROR", "ALARM"),
    ("RECOVER", "RECONNECT"): ("IDLE", "CLEAR"),
    ("ERROR", "RECONNECT"): ("IDLE", "CLEAR"),
}
v = sys.stdin.read().split()
if len(v) != 2 or v[0] not in STATES or v[1] not in EVENTS:
    print("INVALID")
else:
    state, event = v
    if event == "DETACH" and state != "ERROR":
        result = ("RECOVER", "INVALIDATE")
    elif event == "STOP" and state not in {"ERROR", "RECOVER"}:
        result = ("IDLE", "CANCEL")
    else:
        result = TABLE.get((state, event), (state, "IGNORE"))
    print(*result)

더 읽기

면접 질문

  • 센서 기록기의 상태 머신을 설명해 주시면 됩니다.