State Machine
우리가 State Machine을 작성하는 최종 목표는 Event Action Table이다.
Event는 특정 State에서 다른 State로 전환(Transition)을 일으키는 것을 말한다.
즉, Event Action Table은 특정 State에서 다른 State로 전환 될 때 발생하는 Action들을 모아놓은 것이다.
Drawing State Machine
가장 위의 단계부터 그려보면서 내려가보자.
Main Window & Alert Dialogue
기본 창과 알림을 보여주는 창과의 관계부터 시작된다.▼
메인 화면(Main Window)에서 예산 확인 메뉴(Check Budget Menu)를 누르면 예산 확인 화면(Check Budget Window)으로 넘어간다.
그리고 예산 확인 화면에서 창을 닫으면 경고 화면(Alert Diaglogue)이 나오고, OK를 누르면 다시 메인 화면으로, Cancel을 누르면 다시 예산 확인 화면으로 돌아간다.
Check Budget Window 내부
그러면 이제 조금 더 깊이 들어가, 예산 확인 화면의 State를 그려보자. ▼
선택된거랑 선택 안된걸로 state를 구분할 수 있고, 처음 시작은 선택이 되어있지 않으므로 No Client Selected 상태로 시작한다.
유저가 Client를 선택하면 Client Selected로 넘어가게 된다.
"그런데 선택이 됐는데 또 선택을 하면 어떻게 될까?"
Client를 삼성에서 현대로 바꿨다고 해보자.
유저의 입장에서는 바꾼 것으로 인식하지만, 시스템 입장에서는 새로운 것을 선택하고 이를 기존의 것과 바꾼것이다.
하지만 선택했다는 상태 자체에는 변함이 없기에 원래 자리 그대로 돌아가게 된다.
Client Selected 이후
더 깊이 들어가보면 Campaign의 선택에 대한 상태도 볼 수 있다.
이번에도 위와 같이 선택된 상태에서 또 선택하는 것은 값만 바뀐 것이지, 상태는 그대로다.
Campaign Selected 이후
텍스트 필드도 위와 같은 방식으로 볼 수 있다.
체크버튼을 눌러야만 결과가 표시되고, 안누르면 보이지 않는다.
여기서도 체크버튼을 계속 눌러도 연산만 계속할 뿐, 연산 결과를 출력했다는 상태는 그대로이다.
종합
위의 것을 다 합치면 이렇게 된다. ▼
상태 포함
하위 단계는 상위 단계의 상태를 포함한다. ▼
예를 들면 Campaign Selected 상태는 Client Selected상태를 포함한다.
Client를 골라야 다음 단계인 Campaign으로 넘어가기 때문이다.
Halt
어디 있든 간에 closeButton을 갈기면 경고문을 출력해야한다. ▼
어디에서든 상관없이 경고 창을 띄웠는데, 캔슬을 누르면 어디로 가야할까? ▼
클로즈 버튼 갈기기 전 상태로 돌아가야하는데, 정확히 하나를 특정 지을 수도 없고, 모든 곳에 선을 다 그을 수 없으니 아래와 같이 표현한다. ▼
4번 캠페인 선택된 곳에서 체크 버튼을 다시 선택한다고 하면 블랭크로 돌아간다.
화면의 개수
그럼 화면은 몇개일까?
메인, 클라이언트 선택/미선택, 캠페인 선택/미선택 화면으로 총 5개가 존재하게 된다.
Event-Action Table
앞에서도 이야기 했듯이 우리의 목표는 Event-Action Table이다. ▼
Current state |
Event | Action | Next State |
- | 캠페인 예산 메뉴가 선택 되었는지 확인한다. | CheckCampaignBudgetUI를 보여준다. 클라이언트 dropdown을 로딩한다. 캠페인 dropdown을 비활성화한다. 체크버튼을 비활성화한다. 창을 활성화한다. |
1 |
1 | 클라이언트가 선택되었다. | 캠페인 dropdown을 지운다. 캠페인 dropdown을 로딩한다. 예산 텍스트 필드를 지운다. 체크 버튼을 비활성화한다. 창을 활성화한다. |
2 |
2, 3, 4 | 클라이언트가 선택되었다. | 캠페인 dropdown을 지운다. 캠페인 dropdown을 로딩한다. 예산 텍스트 필드를 지운다. 체크 버튼을 비활성화한다. |
2 |
2 | 캠페인이 선택되었다. | 예산 텍스트 필드를 지운다. 체크 버튼을 활성화한다. |
3 |
3 | 체크 버튼이 클릭되었다. | 예산을 계산한다. 결과를 보여준다. |
4 |
3, 4 | 캠페인이 선택되었다. | 예산 텍스트 필드를 지운다. | 3 |
4 | 체크 버튼이 클릭되었다. | 예산을 계산한다. 결과를 보여준다. |
4 |
1, 2, 3, 4 | Close 버튼이 클릭 되었다. | 경고 화면을 보여준다. | 5 |
5 | Ok 버튼이 클릭 되었다. | 닫기 경고 화면을 보여준다. 창을 닫는다. |
- |
5 | 체크 버튼이 클릭되었다. | 닫기 경고 화면을 보여준다. | H* |
Current State에서 정의된 어떤 Event가 발생하면, 그에 맞게 정의 된 Action이 수행되고 Next State로 이동할 지를 표의 형태로 정리한 것이 Event-Action Table이다.
코드 측면
"이런 거 없이 많은 분기 프로그래밍으로 if, else-if의 형태로도 충분하지 않나?"
그렇게 코드를 짜도 코드가 안돌아가는 것은 아니다.
하지만 if, else-if와 같이 분기로만 프로그래밍을 하게 되면 코드의 퀄리티가 굉장히 낮아진다.
조건문으로 프로그래밍을 하게 되면 모듈화가 이루어지지 않으며, 가장 바깥 조건문이 수정될 경우 전체를 수정해야하는 가능성도 생긴다. ▼
예제
자판기
실제 자판기를 그리기에는 너무나도 복잡하기에 조금 간소화된 자판기를 그려보자. ▼
명세
우리가 그리려는 자판기의 명세는 다음과 같다.
- 넣을 수 있는 동전은 100원 하나
- 자판기의 모든 음료는 200원
- 200원 초과로 돈을 투입하게 되면 돈은 반환됨
- 동전 반환 버튼을 누르면 투입한 모든 동전이 반환됨
- 음료를 구매할 수 있는 금액이 투입되면 음료 버튼에 불빛이 들어옴
- 충분한 금액이 투입된 상태에서 음료 버튼을 누르면 음료를 내보냄
Event와 Action 정의
위의 명세를 모두 파악했다면, 명세를 토대로 모든 Event와 Action을 정리한다. ▼
Event
1. 동전 투입(100원)
2. 동전 반환 버튼 누르기
3. 음료 버튼 누르기
Action
A. 투입한 금액 보여주기
B. 동전 반환 해주기
C. 음료 버튼의 불빛
D. 음료 내보내기
State Machine 그리기
모든 Event와 Action의 정의가 끝났다면 이제 State Machine을 그리면 된다.
State의 이동을 표시하고, 그에 맞게 Event와 Action을 [Event #/Action #]의 형태로 표기해주면 된다.
그러면 아래와 같이 State Machine이 그려진다. ▼
계산기
역시나 실제 계산기의 State Machine은 너무나도 복잡하고 그 양이 방대하기에 간소화된 기능만 하는 계산기를 그려보자. ▼
명세
우리가 그리려는 계산기의 명세는 다음과 같다.
- 0부터 9까지의 숫자만 입력 가능
(단, 09나 0009와 같이 입력할 경우 앞의 0은 무시) - 연산자는 사칙 연산만 입력 가능
- 숫자 → 연산자 → 숫자 의 순서로 입력이 가능
- 등호를 누르면 연산 결과 출력
Event와 Action 정의
위의 명세를 모두 파악했다면, 명세를 토대로 모든 Event와 Action을 정리한다. ▼
Event
1. 숫자 (0 ~ 9)
2. 연산자 (+, -, /, x)
3. 등호 (=)
Action
A. 입력 값 그대로 출력
B. 이전 연산자를 새 입력 값으로 대체
C. 연산 결과 출력
D. 이전 화면 지우고 입력 값 출력
State Machine 그리기
모든 Event와 Action의 정의가 끝났다면 이제 State Machine을 그리면 된다.
State의 이동을 표시하고, 그에 맞게 Event와 Action을 [Event #/Action #]의 형태로 표기해주면 된다.
그러면 아래와 같이 State Machine이 그려진다. ▼
코드 작성해보기
State와 Event enum
첫번째 박스
State와 Event를 열거형으로 표시.
// State 열거형 정의
typedef enum {
STATE_A,
STATE_B,
...
STATE_END,
} State;
// Event 열거형 정의
typedef enum {
EVENT_1,
EVENT_2,
EVENT_3,
...
} Event;
EventActionTable struct
앞에서도 이야기 했지만, 우리의 State Machine 디자인의 최종 목표는 Event Action table이었다.
Event Action Table을 앞에서 명시했으니 이를 코드에서 사용할 차례다.
EventActionTable의 한 행을 구조체로 정의를 해놓고, 이를 배열로 사용하면 위의 표와 같은 형태로 코드에서 사용할 수 있다. ▼
typedef struct {
State curState;
Event event;
void (*action)(void);
State nextState;
} EventActionTable;
Define Action Functions
위의 EventActionTable의 구조체의 변수로 전달할 함수를 정의해야한다.
함수는 원래 정의하던 대로 정의하면 되나, 함수 포인터의 리턴타입과 매개변수가 void 이므로 아래와 같은 형태로 정의와 구현을 해야한다. ▼
void action_1() {};
void action_2() {};
void action_3() {};
Initialize EventActionTable
이제 넣을 함수도 전부 정의했다면, EventActionTable을 초기화 할 차례다.
위에서 적은 내용대로 표를 배열의 형태로 만들어주면 된다.▼
EventActionTable Table[] = {
{STATE_A, EVENT_1, action_1, STATE_B},
{STATE_B, EVENT_2, action_2, STATE_A},
...
};
Run
void runStateMachine() {
...
curState = STATE_A;
while (curState != STATE_END) {
curEvent = getNextEvent();
for (i = 0; i < NUMBER_OF_TABLE_ENTRIES; i) {
if (curState == table[i].curState) {
if (curEvent == table[i].event) { (table[i].action)();
curState = table[i].nextState;
break;
}
}
}
}
}
현재 상태를 curState
변수로 알고 있으므로, for문을 돌면서 EventActionTable에 현재 상태와 같은 상태를 찾는다.
만약에 같은 상태가 있으면 거기에 정의된 Action(함수 포인터로 전달한 함수)을 실행한다.
'CS > 소프트웨어 공학' 카테고리의 다른 글
19. Software Architecture (0) | 2024.02.04 |
---|---|
18. Refining The Requirements Model (0) | 2024.02.04 |
16. Designing Boundary Classes (작성중) (0) | 2024.02.03 |
15. Detailed Design (0) | 2024.02.03 |
14. Sequence Diagram (작성중) (0) | 2024.02.03 |