지금까지 requirement description 을 기반으로 커뮤니케이션 다이어그램 또는 시퀀스 다이어그램을 그린 뒤, 이 결과를 클래스 다이어그램으로 변환하는 모든 과정을 살펴보았다.
이번 글에서는 사용자 UI 를 설계하는 과정을 더 자세히 정리한다.
우리는 시스템 아키텍처를 설계할 때 boundary class, control class, entity class 3가지 종류로 구분하여 설계하였다.
이를 가리켜 3-tier architecture 라고도 부른다.
boundary class 는 presentation layer 라고도 부르며,
사용자 또는 다른 외부 시스템에서 사용하는 인터페이스를 다루고,
사용자가 데이터를 입력할 수 있는 매커니즘을 제공하며,
인터페이스에 가지고 있는 데이터를 적절하게 포맷팅하여 보여주는 역할을 수행한다.
boudary class 를 개발하는 방법은 크게 4가지 단계로 수행된다.
1. user interface 프로토타입을 만든다.
하나의 기능을 수행하기 위한 UI 는 다양하게 나올 수 있다.
따라서 다양한 프로토타입을 만들고 테스트하면서 제일 좋은 UI 를 선별한다.
2. class 를 설계한다. (attribute 설계)
선택된 UI 화면을 기반으로 attribute 를 설계한다.
3. interface 의 상호작용을 모델링한다. (operation 설계)
시퀀스/커뮤니케이션 다이어그램을 기반으로 boundary 클래스가 다른 control, entity 클래스와 어떻게 상호작용하는지 파악한다.
4. state machine 을 사용하여 interface 의 컨트롤을 설계한다.
필요한 경우 (하지만 심플한 UI가 아니라면 대부분) state machine 을 사용하여 UI 동작을 표현한다.
프로토타입 만들기
프로토타입은 완성된 결과물처럼 보이면서 부분적으로 그렇게 동작하지만 특정 기능은 존재하지 않는 모델을 말한다.
정산 시스템을 만들기 위해 식당의 특정 메뉴가 얼마나 판매되었는지 확인하는 UI 를 만든다고 해보자.
그러면 예를 들어 같이 3가지 형태의 UI 가 나올 수 있다.
세 UI 모두 사용자로부터 식당, 메뉴를 선택받고 판매금을 계산하는 기능을 수행하도록 안내하는데는 문제가 없다.
따라서 각 프로토타입을 기반으로 사용자 테스트 등을 진행하여 적합한 UI 를 하나 선정한다.
Boundary Class 설계
1. collaboration
이제 이 UI 를 선택했다고 가정하고 이 UI 에 대한 boundary class 를 설계해보자.
클래스 다이어그램을 그리는 과정은 앞서 했던 과정과 동일하게 collaboration 부터 시작한다.
먼저 러프하게 이렇게 클래스를 설계할 수 있다.
판매금 계산 UI 클래스, control 클래스 와 식당, 메뉴 엔티티 클래스가 필요하다.
이때 boundary class 의 개수는 이 기능을 수행하는데 필요한 화면 (윈도우) 의 개수와 똑같다.
만약 이 화면을 골랐다면 각 검색버튼을 클릭했을 때 보일 윈도우 화면 2개와, 계산하는 화면 1개
총 3개의 윈도우가 필요하므로 UI 클래스도 3개가 필요할 것이다.
다시 이 구조로 돌아와서,
이 화면에서 식당을 선택할 리스트를 세팅하고, 식당이 선택되었을 때 메뉴 리스트를 세팅하는 과정까지 CalcuateSalesAmount 컨트롤 클래스가 맡으면 너무 많은 책임을 지게 된다.
CalcuateSalesAmount 컨트롤 클래스는 계산 버튼이 클릭되었을 때 판매금을 계산하는 책임에 집중하게 두고,
리스트를 세팅하는 책임은 다른 컨트롤 클래스로 분리해두면 응집도가 더 올라갈 것이다.
또한 식당을 선택하는 과정에서 Restauratnt 엔티티 클래스도 추가로 필요하다.
따라서 위와 같이 collaboration 을 그릴 수 있다.
2. class attribute
이 UI 화면을 구성하는 요소를 살펴보면
식당, 메뉴, 판매금 글자를 보여줄 Label 3개
식당, 메뉴를 선택할 Choice 2개
판매금 정보를 보여줄 TextField 1개
계산, 닫기 버튼에 해당하는 Button 2개 가 필요하다.
그리고 이 모든 UI 요소는 하나의 윈도우 창 (Dialog) 에 들어있다.
이를 기반으로 클래스 다이어그램을 간단하게 그리면 위와 같이 그릴 수 있다.
UI 화면은 Dialog 클래스를 상속받았으며, 이 내부에 배치된 Label, Choice, TextField, Button 컴포넌트는 창이 닫히면 모두 함께 사라지므로 composition 관계로 나타낼 수 있다.
이를 기반으로 attribute 를 그리면 위와 같이 그릴 수 있다.
3. class operation
다음으로 바운더리 클래스의 operation 을 찾아보자.
operation 을 찾는 과정은 앞서 했던 것과 마찬가지로 커뮤니케이션 다이어그램 또는 시퀀스 다이어그램을 그려서 찾을 수 있다.
먼저 엔티티 클래스만으로 이루어진 시퀀스 다이어그램을 그려보자.
먼저 restaurant 클래스에서 이름을 가져와 현재 사용자가 선택한 레스토랑 정보를 보여준다.
다음으로 해당 레스토랑에 있는 메뉴 리스트를 보여주기 위해 listMenu() 메서드를 호출한다.
사용자가 리스트에서 메뉴를 선택하면 해당 calculateSalesAmounts() 메서드를 호출하여 해당 메뉴의 옵션 별 가격을 조회하여 판매금을 계산한다.
이제 엔티티 클래스에 더해 바운더리 클래스와 컨트롤 클래스까지 포함한 시퀀스 다이어그램을 그려보자.
먼저 사용자가 '판매금 계산' 메뉴를 실행하면 컨트롤 클래스가 먼저 생성해야 한다.
따라서 컨트롤 클래스의 생성자를 먼저 호출하여 컨트롤 클래스 인스턴스를 만든다.
이후 로직은 모두 컨트롤 클래스의 생성자 내에서 처리된다.
먼저 자신의 참조를 넘기면서 바운더리 클래스의 인스턴스를 생성한다.
이 인스턴스의 참조는 csaUI 라는 컨트롤 클래스의 멤버 변수에 할당된다.
(이 결과 두 클래스는 서로의 레퍼런스를 교환하게 된다)
다음으로 ListRestaurant 컨트롤 클래스의 인스턴스를 생성하여 restaurant choice 내부 항목을 채우도록 한다.
이 인스턴스는 컨트롤 클래스의 lr 멤버 변수에 할당된다.
다음으로 ListRestaurant 클래스의 listAllRestaurants() 메서드를 호출한다.
메서드의 매개변수로 csaUI 에 저장했던 바운더리 클래스의 참조를 넘긴다.
listAllRestaurants() 메서드 내에서는 자신이 알아낸 식당 이름 리스트 데이터를 하나하나 UI 항목에 추가한다.
이 기능은 바운더리 클래스의 addRestaurantName() 메서드를 활용하여 진행한다.
모든 식당 이름이 세팅되었다면 이제 사용자가 UI 화면을 사용할 수 있도록 enable() 메서드를 호출하여 활성화시킨다.
식당 리스트가 세팅된 이후 사용자가 식당을 선택하면 위 다이어그램의 로직이 실행된다.
위 그림에서 selectRestaurant 는 함수 이름이 아니라 유저의 액션을 나타낸다.
사용자가 식당을 하나 선택하면 바운더리 클래스는 컨트롤 클래스의 restaurantSelected() 메서드를 호출한다.
그 내부에서는 바운더리 클래스로부터 선택된 식당을 가져온 뒤, aRestaurant 변수에 저장한다.
다음으로는 ListMenu 인스턴스를 만들어 lm 멤버 변수에 저장하고, listMenu() 메서드를 호출하여 menuChoice 에 메뉴 이름을 세팅한다. 이때 listMenu() 메서드에는 csaUI 바운더리 클래스 참조와 aRestaurant 식당 인스턴스 참조를 넘긴다.
그런데 사실 UI 클래스의 동작을 더 정확하게 나타내면 위와 같다.
먼저 사용자가 식당을 선택하는 동작은 'Choice' 컴포넌트에 대해서 수행하고,
Choice 컴포넌트는 상태가 바뀌면 자신의 상태가 바뀌었다는 것을 event 로 만들어서 메인 윈도우 (Dialog) 에 itemStateChanged(evt) 함수를 호출하여 알린다.
이는 Dialog 클래스를 상속하고 있는 CalcualteSalesAmountUI 클래스가 메세지를 받을 것이다.
이때 상태가 바뀌었다는 '이벤트' 는 모든 컴포넌트가 공통으로 사용하는 것이다.
따라서 이 이벤트가 누구로부터 왔는지 event 의 source 속성을 통해 파악한다.
만약 이 이벤트가 restaurantChoice 가 보낸 이벤트라면 컨트롤 클래스의 clientSelected() 메서드를 호출하여 앞서 그린 시퀀스 다이어그램의 로직을 수행한다.
그리고 사용자가 식당을 바꿀 수 있다는 것을 고려하면 최종 다이어그램은 위와 같이 그릴 수 있다.
메뉴 리스트를 세팅하기 전에 이전에 세팅된 메뉴 리스트를 지우는 clearAllMenuNames() 메서드를 호출하는 과정이 추가되었다.
마지막으로 사용자가 메뉴를 선택하면 (selectMenu) 컨트롤 클래스의 menuSelected() 메서드를 호출한다.
메서드 안에서는 '계산' 버튼을 활성화 시키는 enableCalcuateButton() 메서드를 호출한다.
사용자가 계산 버튼을 클릭하면 (calcuate) 컨트롤 클래스의 calculateSalesAmount() 메서드를 호출한다.
메서드 내에서는 선택된 메뉴를 가져온 뒤, 해당 메뉴의 판매금을 Menu, Option 클래스의 조합으로 계산하여 가져온다.
그리고 그 결과를 바운더리 클래스의 setAmount() 메서드를 호출하여 세팅한다.
이로서 특정 메뉴의 판매금을 계산하는 화면의 모든 로직을 다 기술하였다.
이제 시퀀스 다이어그램에서 등장한 메세지 전달을 메서드로 만들어 클래스 다이어그램에 기술해주면 된다.
그러면 위와 같이 메서드를 기술할 수 있다.
4. state machine
마지막으로 바운더리 클래스의 동작을 state machine 으로 표현해보자.
(필요한 경우에만 한다고 하지만, 대부분 이 과정까지 필요하다)
Horrock 이라는 사람이 제안한 방법에 따르면 UI 동작을 state machine 으로 표현하는 과정은 크게 5가지 단계로 구성된다.
1. high-level requirements, main user task 를 먼저 묘사한다.
2. user interface 동작을 묘사한다.
3. user interface 규칙을 정의한다.
4. state machine 을 그린다.
5. event action table 을 그린다.
여기에서 중요한 것 (우리가 원하는 것) 은 event action table 이다.
이 결과물만 나오면, UI 클래스의 동작을 코드로 그대로 옮길 수 있다.
1. high-level requirements
지금 보고 있는 식당 관리 시스템으로 생각해보면,
판매금 계산 기능의 목적은 특정 메뉴가 팔린 전체 금액이 얼마인지 아는 것이다.
이 값은 특정 메뉴의 각 옵션별 판매 수량과 금액을 곱한 값을 모두 더해서 구한다.
2. user inteface 동작
다음으로 UI 동작을 정의해보자.
- restaurant dropdown 은 모든 식당 리스트를 보여준다.
식당이 하나 선택된 이후에는 그 식당에서 제공하는 모든 메뉴를 menu dropdown 에서 보여준다.
- menu dropdown 은 restaurant dropdown 에서 선택한 식당의 모든 메뉴 리스트를 보여준다.
menu dropdown 에서 특정 메뉴를 선택하면 '계산' 버튼이 활성화된다.
- amount textfield 는 menu dropdown 에서 선택한 메뉴의 전체 판매금을 보여준다.
- 계산 버튼은 클릭시 현재 선택된 메뉴의 전체 판매금을 계산하는 로직을 실행시킨다.
- 종료 버튼은 클릭시 현재 창을 닫고 use case 를 종료한다.
3. UI 규칙 정의
- constant behavior
restaurant dropdown 은 항상 정해진 동작을 수행한다.
식당을 선택할 때마다, 해당 식당의 모든 메뉴를 menu dropdown 에 리스트업한다.
amount textfiend 는 처음에 비어있다.
이 필드 값은 사용자가 수정할 수 없으며, 새로운 식당 또는 메뉴가 선택될 때마다 비워진다.
닫기 버튼은 항상 선택 가능하며, 선택할 때마다 항상 현재 창을 닫는 동작을 수행한다.
- varying behavior
menu dropdown 은 처음에 비활성화 되어있다.
사용자가 식당을 선택하면 그 이후에 메뉴를 선택할 수 있다.
한번 식당이 선택된어 메뉴 리스트가 채워진 이후로는 항상 활성화 되어있다.
check button 은 처음에는 비활성화 되어있다.
사용자가 메뉴를 선택하면 활성화되고, 식당을 새로 선택하면 비활성화된다.
- 시작 및 종료 이벤트
이 창은 '메뉴별 판매금액 계산' 메뉴 아이템을 선택하면 만들어진다.
종료 버튼을 클릭하면 '정말 종료하시겠습니까?' 라는 메세지가 적힌 경고창이 뜬다.
경고창에는 '확인' 버튼과 '취소' 버튼 2개가 있으며, 확인 버튼을 클릭하면 '메뉴별 판매금액 계산' 창을 닫는다.
취소 버튼을 클릭하면 취소 버튼을 클릭했을 때의 상태로 되돌아간다.
4. state machine 그리기
이제 위에서 작성한 '규칙' 을 기반으로 상태 기계를 그려보자.
제일 먼저 top-level 의 스테이트 머신을 그린다.
top level 이라는 것은 우리가 보여줄 그 화면에 들어가는 로직 (entry) 과 나가는 로직 (exit) 을 먼저 표현하는 것이다.
그리고 그 화면 내에서 실행될 상태 변화 로직은 그 상태 안에서 기술할 것이다.
그림으로는 이렇게 그릴 수 있다.
먼저 프로그램이 실행되는 Main Window 상태에서 시작한다.
만약 calculateSalesAmount 메뉴를 선택하는 'calculateSalesAmountMenuSelected' 이벤트가 발생하면
상태는 CalculateSalesAmountWindow 상태로 천이한다.
CalculateSalesAmountWindow 상태에서 닫기 버튼을 클릭하는 closeButtonClicked 이벤트가 발생하면
AlertDialogue 상태로 천이한다.
만약 이 상태에서 확인 버튼을 클릭하면 MainWindow 상태로 되돌아가고,
취소 버튼을 클릭하면 이전 CalcualteSalesAmountWindow 상태로 되돌아간다.
이제 CalculateSalesAmountWindow 상태를 더 자세히 표현해보자.
이 상태 안(nested)에서도 여러가지 상태가 존재하며 다양한 이벤트에 따라 상태가 변화할 수 있다.
처음으로 calcualte sales amount window 로 들어가면 처음에는 아무런 식당도 선택되지 않은 상태로 시작한다.
한번 식당을 선택하고나면 Restaurant Selected 상태로 천이하며 (transition)
이후에 식당을 계속해서 선택해도 restaurant selected 상태를 유지한다.
상태가 2개로 나뉜다는 것은, 각 상태마다 사용자가 보는 화면이 다르다는 뜻과 같다.
식당이 선택되지 않은 상태에서는 메뉴 선택 드롭다운, 계산 버튼이 비활성화되어있다.
식당을 선택한 상태에서는 메뉴 선택 드롭다운이 새롭게 활성화된다.
다음으로는 식당을 선택했을 때 그 안에서 다시 세부적인 상태 변화를 나타내보자.
식당을 선택한 이후에는 메뉴를 선택해야 한다.
처음에는 아무런 메뉴가 선택되지 않은 상태였다가, 한번 메뉴를 선택한 이후에는 메뉴가 선택된 상태가 유지된다.
만약 식당을 새로 선택하게 되면 다시 메뉴가 선택된 상태가 초기화되면서 메뉴는 선택되지 않은 상태로 되돌아 갈 것이다.
이제 메뉴가 선택된 이후에 판매금 text field 의 상태를 보자.
처음에는 text field 가 비어있는 상태로 시작한다.
이 상태에서 '계산' 버튼을 클릭하면 계산 결과가 보여질 것이다.
이 상태에서 다시 '계산' 버튼을 클릭할 수 있으며, 다시 결과를 보여줄 것이다.
지금까지 단편적으로 그린 상태 기계를 하나의 그림에 합쳐서 그리면 위와 같이 그릴 수 있다.
top level 상태 안에 상태 기계를 그리는 방식으로 합쳐서 그린 것이다.
cancel 버튼을 클릭했을 때 가는 H* 라는 것은 history 를 뜻한다.
이때 되돌아 갈 수 있는 상태는 No Restaurant Selected, No Menu Selected, Blank, DisplayResult 4가지 상태가 있다.
이 4가지 상태 모두에서 '취소' 버튼을 클릭할 수 있으며, 그 버튼을 클릭했을 때는 내가 직전에 있었던 상태로 되돌아간다는 뜻이다.
(*로 돌아갈 상태의 번호를 지정하지 않고 일반화해서 표현한 것)
그리고 이렇게 하나로 합쳐서 그렸을 때 다른 상태 기계를 포함하고 있는 상태에 꼭 이름을 붙일 필요는 없다.
그래서 상태기계는 최종적으로 이렇게 그릴 수 있다.
5. Event-Action table
위 그림에서 각 상태에 1부터 5까지 번호를 붙인 것을 확인할 수 있다.
이제 이 번호를 이용해서 어떤 상태일 때, 어떤 이벤트가 발생하면, 어떤 액션을 취하고, 어떤 상태로 넘어가는지를 표로 그려보자.
current state | event | action | next state |
- | Calculate Sales Amount 메뉴 선택 |
CalculateSalesAmountUI 를 출력한다. restaurant dropdown 내용을 로딩한다. menu dropdown 과 '계산' 버튼은 비활성화되어있다. 윈도우를 활성화한다. |
1 |
1 | 식당을 선택한다 | menu dropdown 을 다 지운다. menu dropdown 내용을 로딩한다. menu dropdown 을 활성화한다. |
2 |
2, 3, 4 | 식당을 선택한다 | menu dropdown 을 다 지운다. menu dropdown 내용을 로딩한다. 판매금 textfield 를 비운다. 계산 버튼을 비활성화한다. |
2 |
2 | 메뉴를 선택한다 | 판매금 textfield 를 비운다. 계산 버튼을 활성화한다. |
3 |
3 | 계산 버튼을 클릭한다. | 판매금을 계산한다. 계산 결과를 판매금 textfield 에 채운다. |
4 |
4 | 계산 버튼을 클릭한다. | 판매금을 계산한다. 계산 결과를 판매금 textfield 에 채운다. |
4 |
3, 4 | 메뉴를 선택한다. | 판매금 textfield 를 비운다. | 3 |
1, 2, 3, 4 | 닫기 버튼을 클릭한다. | 경고 dialog 를 출력한다. | 5 |
5 | OK 버튼을 클릭한다. | 경고 dialog 를 닫는다. 윈도우를 닫는다. |
- |
5 | cancel 버튼을 클릭한다. | 경고 dialog 를 닫는다. | H* |
이 표를 가리켜 event-action table 이라고 부르며, 각 UI 마다 이 테이블을 그려주면 된다.
그리고 구현할 때는 이 표를 기반으로 UI 를 구현하면 된다.
만약 이렇게 state machine 을 그리지 않으면,
UI 기능을 구현할 때 if-else 구문을 나열하여 사람이 조건을 직접 나열하여 구현해야 한다.
이렇게 구현해놓으면 UI 가 바뀔 때마다 코드의 많은 부분을 수정해야 하므로 유지보수에 좋지 않다.
(그리고 UI 는 자주 바뀐다)
하지만 state machine 으로 표현하고 event table 을 기반으로 구현해두면
기능이 바뀌었을 때, 그 내용에 맞게 state machine 을 수정하고 event table을 수정한다.
그러면 우리는 기존 event table 기반으로 구현했던 내용에서 바뀐 내용만 선택하여 수정하면 되므로 유지보수가 더 용이해진다.
마지막으로 state machine 에서 작성한 로직을 기반으로 시퀀스 다이어그램은 위와 같이 수정할 수 있다.
처음 restaurant dropdown 세팅 로직에서 식당을 선택 전에 메뉴 list dropdown 과 '계산' 버튼을 비활성화하는 동작까지 추가로 기술하였다.
다음 글에서는 실제 예시를 기반으로 어떻게 event-action 테이블을 그리고 코드로 옮기는지
그 과정을 정리해본다.
'CS > 소프트웨어공학' 카테고리의 다른 글
[소프트웨어공학] 17. State Machine 예제 (수정예정) (0) | 2025.06.07 |
---|---|
[소프트웨어공학] 15. Detailed Design (2) | 2025.06.05 |
[소프트웨어공학] 14. Object Interaction (0) | 2025.06.04 |
[소프트웨어공학] 13. Requirement Analysis (4) | 2025.06.03 |
[소프트웨어공학] 12. Configuration and Version Management (0) | 2025.04.21 |