상태 기계
고친 사람 github-actions[bot]
상태 기계는 지금 어떤 상태인지를 기억해 둡니다. 들어온 사건은 그 상태에 맞춰 다르게 처리합니다. 프로그램이 거쳐 가는 단계를 적을 때 씁니다. 그래픽스 라이브러리처럼 켜 둔 설정을 기억해 두는 물건도 같은 이름으로 부릅니다.
쉽고 빠른 이해
상태 기계는 프로그램이 지금 어느 단계에 있는지를 값 하나로 들고 다니는 방식입니다. 주문이 접수됨·결제됨·배송중·완료 넷 중 하나를 오가는 것이 그런 값입니다.
이 값이 없으면 「이미 취소된 주문을 출고해도 되나」를 물을 때마다 조건문을 새로 짭니다. 조건이 코드 곳곳에 흩어지면 빠뜨린 조합이 생깁니다.
어떻게 도는가:
- 지금 상태를 읽습니다
- 들어온 사건과 짝지어 다음 상태를 찾습니다
- 짝이 없으면 그 사건을 받지 않고 버립니다
대가가 있습니다. 상태를 하나 늘리면 챙겨야 할 짝이 사건 수만큼 늘어납니다. 단계가 둘뿐인 흐름에 들이면 표만 생기고 얻는 것이 없습니다.
상세
신호등을 떠올리면 됩니다. 지금 켜진 색이 상태입니다. 시간이 다 된 것이 사건입니다. 빨강에서 초록으로 넘어가는 것이 전이입니다.
다만 신호등은 시간만 보고 넘어갑니다. 프로그램의 상태 기계는 대개 바깥에서 들어온 요청이나 응답을 받아 넘어갑니다.
상태 기계는 세 가지를 미리 적어 둔 것입니다. 들어갈 수 있는 상태의 목록, 받아들일 사건의 목록, 그리고 「어느 상태에서 어느 사건을 받으면 어디로 간다」는 짝의 목록입니다.
다음에 무엇을 할지는 지금 상태와 들어온 사건 둘만 보고 정합니다. 거기까지 어떤 길로 왔는지는 보지 않습니다. 그래서 프로그램이 기억해 둘 것이 상태 값 하나로 줄어듭니다.
상태 · 사건 · 전이
상태는 「지금 어느 단계인가」를 가리키는 이름입니다. 이름을 미리 정해 두어야 코드 곳곳에서 같은 뜻으로 읽힙니다.
사건은 밖에서 들어와 상태를 흔드는 것입니다. 이벤트라고도 부릅니다. 사용자의 클릭, 도착한 메시지, 다 된 타이머가 전부 사건입니다.
전이는 「이 상태에서 이 사건을 받으면 저 상태로 간다」는 한 줄입니다. 앞에서 짝이라고 부른 것이 이것입니다. 적어 두지 않은 전이는 일어나지 않습니다. 이것이 상태 기계가 주는 보장입니다.
온라인 주문을 상태 기계로 그리면 이렇습니다.
stateDiagram-v2
[*] --> 접수됨
접수됨 --> 결제됨: 결제 승인
접수됨 --> 취소됨: 취소 요청
결제됨 --> 배송중: 출고
결제됨 --> 취소됨: 취소 요청
배송중 --> 완료: 수령 확인
완료 --> [*]
취소됨 --> [*]
그림의 [*] 는 흐름의 시작과 끝입니다. 주문은 접수됨으로 들어와 완료나 취소됨에서
끝납니다.
배송중에서 취소됨으로 가는 화살표가 없다는 점이 이 그림의 요점입니다. 배송이 시작된 주문에 취소 요청이 들어오면 상태 기계는 그 요청을 받지 않습니다. 「배송 중에는 취소가 안 된다」는 규칙을 문장이 아니라 그림으로 적어 둔 것입니다.
빠진 조합을 드러내는 전이표
화살표를 표로 옮기면 안 적은 것이 눈에 보입니다. 세로는 상태, 가로는 사건입니다.
| 결제 승인 | 출고 | 취소 요청 | |
|---|---|---|---|
| 접수됨 | 결제됨 | — | 취소됨 |
| 결제됨 | — | 배송중 | 취소됨 |
| 배송중 | — | — | — |
| 완료 | — | — | — |
| 취소됨 | — | — | — |
빈칸은 「이 상태에서 그 사건은 받지 않는다」는 뜻입니다. 완료와 취소됨은 끝난 상태라 받을 사건이 하나도 없어 행 전체가 빈칸입니다. 같은 규칙을 조건문으로 흩어 놓으면 이 빈칸이 안 보입니다. 빠뜨린 조합은 대개 그렇게 생깁니다.
코드로 옮길 때도 전이를 한 곳에 모읍니다. 조건문을 늘어놓는 대신 목록을 하나 만듭니다. 거기에 없는 조합은 상태를 바꾸지 않습니다.
전이표 = {
("접수됨", "결제 승인"): "결제됨",
("접수됨", "취소 요청"): "취소됨",
("결제됨", "출고"): "배송중",
}
def 다음(상태, 사건):
return 전이표.get((상태, 사건), 상태)
다음("접수됨", "결제 승인") # 결제됨
다음("배송중", "취소 요청") # 배송중
마지막 줄은 짝이 없어 상태가 바뀌지 않았습니다. 배송 중인 주문의 취소를 막는 조건문을 따로 쓰지 않았는데도 막혔습니다. 표에 안 적었다는 것이 곧 금지입니다.
멈췄다 이어 가는 함수
여기서 보는 것은 중간에 멈췄다 이어 가는 함수 하나입니다. async 와 await 로 적는 함수가
그렇습니다. 이렇게 멈췄다 이어 가는 함수를 코루틴이라고 부릅니다. 함수 하나가 어떻게
상태 기계가 되는지를 따라갑니다.
함수가 중간에 멈췄다 이어 가려면 「어디까지 갔나」를 어딘가에 적어 둬야 합니다. 코루틴을 지원하는 언어는 그 일을 컴파일러에게 맡깁니다.
컴파일러는 멈출 수 있는 지점마다 번호를 붙입니다. 그 번호와 지역 변수를 한 객체에 담아 둡니다. 다시 부르면 그 번호를 보고 멈춘 지점부터 이어 갑니다. 함수 하나가 상태 기계 하나가 되는 셈입니다.
이때의 상태는 「지금 몇 번 지점에서 멈춰 있나」입니다. 사건은 「다시 부르는 것」 하나뿐입니다. 그래서 갈림이 없고 지점 0, 1, 2 순으로만 옮겨 갑니다.
stateDiagram-v2
시작 --> 지점0
지점0 --> 지점1: 다시 호출
지점1 --> 지점2: 다시 호출
지점2 --> 끝: 다시 호출
앞의 주문 그림은 사건이 여럿이라 갈래가 있었습니다. 이 그림은 화살표 라벨이 「다시 호출」 하나뿐이라 일직선입니다. 이 상태 기계는 사람이 설계한 것이 아니라 컴파일러가 만들어 낸 것입니다.
그래픽스 라이브러리가 말하는 상태 기계
그림을 그리는 라이브러리인 OpenGL 과 WebGL 을 두고도 「상태 기계로 동작한다」고 말합니다. 이때의 상태는 앞에서 본 것처럼 이름이 붙은 유한한 목록이 아닙니다. 지금 켜 둔 설정 전부를 뭉뚱그려 부르는 말입니다.
그리기 함수는 무엇을 어떻게 그릴지를 인자로 다 받지 않습니다. 먼저 설정을 켜 두고 그 다음 그리기 함수를 부르면 켜 둔 설정대로 그립니다. 한 번 켠 설정은 누가 끄기 전까지 다음 그리기에도 걸립니다.
sequenceDiagram
participant 앱
participant 그래픽스라이브러리
앱->>그래픽스라이브러리: 설정 A 를 켠다
앱->>그래픽스라이브러리: 그린다
Note over 그래픽스라이브러리: A 로 그린다
앱->>그래픽스라이브러리: 설정 B 로 바꾼다
앱->>그래픽스라이브러리: 그린다
Note over 그래픽스라이브러리: 이번에는 B 로 그린다
두 번째 화살표와 네 번째 화살표는 같은 그리기 호출입니다. 사이에서 설정을 바꿨다는 것만으로 그려지는 것이 달라집니다.
앞 절들의 상태 기계와 이 쓰임은 두 가지가 같습니다. 물건이 지금 상태를 기억합니다. 같은 호출이 상태에 따라 다르게 움직입니다.
다른 점은 상태를 미리 세어 적을 수 있느냐입니다. 주문 예는 넷을 다 적을 수 있습니다. 그래픽스 설정은 그렇게 못 합니다.
상태가 늘 때 치르는 대가
상태 하나를 늘리면 그 상태에서 받을 사건마다 짝을 새로 정해야 합니다. 상태 다섯에 사건 다섯이면 살펴볼 칸이 스물다섯입니다. 표가 이렇게 커지는 것을 상태 폭발이라고 부릅니다.
흔한 완화책은 세는 일을 상태에서 빼는 것입니다. 「재시도 한 번 함」·「두 번 함」·「세 번 함」으로 상태를 쪼개지 않습니다. 상태는 「재시도 중」 하나로 둡니다. 횟수는 변수에 담습니다.
쪼개면 이렇습니다.
stateDiagram-v2
재시도1회 --> 재시도2회: 실패
재시도2회 --> 재시도3회: 실패
재시도3회 --> 포기: 실패
횟수를 변수로 빼면 이렇게 접힙니다.
stateDiagram-v2
재시도중 --> 재시도중: 실패, 횟수 하나 올림
재시도중 --> 포기: 횟수가 상한에 닿음
note right of 재시도중: 횟수는 변수에 담는다
상태 셋이 하나로 줄고 화살표는 자기 자신으로 도는 것 하나가 됐습니다. 이렇게 곁들이는 변수를 확장 상태라고 부릅니다.
상태 기계를 안 쓰는 쪽도 있습니다. 단계가 둘뿐이거나 한 줄로 흘러 되돌아오지 않는 일에 상태 기계를 들이면 전이표만 생기고 읽을 것이 늘어납니다.
반대로 같은 요청이 때에 따라 받아들여지기도 하고 거절되기도 하는 일에서는 상태 기계가 값을 합니다. 연결을 맺고 끊는 프로토콜, 문자를 하나씩 읽어 판정하는 정규 표현식 엔진, 결제와 배송을 오가는 주문 흐름이 그렇습니다.
flowchart TD
A["되돌아오는 단계가 있나"] -->|아니오| B["안 들인다"]
A -->|예| C["같은 요청이 때에 따라 받아들여졌다 거절되나"]
C -->|아니오| B
C -->|예| D["들인다"]
두 물음에 모두 「예」가 나올 때만 상태 기계가 값을 합니다.
관련 항목
상태 기계를 이루는 구성 요소
상태 전이 · 전이표 · 초기 상태 · 종료 상태 · 확장 상태 · 이벤트
상태 기계의 하위 종류
결정적 유한 오토마타 · 비결정적 유한 오토마타 · 계층 상태 기계 · 푸시다운 오토마타 · 튜링 기계 · 무어 기계 · 밀리 기계
상태 기계로 구현되는 언어 기능
코루틴 · 제너레이터 · 이터레이터 · 이벤트 루프 · 논블로킹 입출력 · 컴파일러
상태 기계로 그리는 통신·제어 흐름
프로토콜 · TCP · 핸드셰이크 · 서킷 브레이커 · 재시도 · 타임아웃
전역 상태를 두고 갈린 그래픽스 라이브러리
OpenGL · WebGL · WebGPU · Vulkan
상태 기계를 여러 대로 복제하는 분산 기법
복제 상태 기계 · 합의 · Raft · Paxos · 복제 로그
상태 기계를 글로 적는 표기법
다른 이름: state machine · finite state machine · 유한 상태 기계 · 스테이트 머신 · FSM