일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 그래프
- 이진탐색
- 스토어드 프로시저
- MYSQL
- union find
- Trie
- SQL
- Hash
- 다익스트라
- binary search
- Stored Procedure
- String
- Brute Force
- Two Points
- DP
- two pointer
- Dijkstra
- Today
- Total
codingfarm
2. 다이어그램으로 작업하기 본문
1. 행위
UML 다이어그램을 만들때에는 행위(behavior)부터 시작하는것이 좋다.
즉, 간단한 시퀀스 다이어그램을 통해 프로그램의 흐름이 어떤식으로 이루어지는지 부터 도식화하자.
도식화의 순서는 주로
- sequence diagram(프로그램 흐름 순서)
- class diagram(클래스간의 관계)
위 순서를 통해 그린 후, 개선이 이루어진다.
예
휴대전화를 제어하는 소프트웨어가 있다.
숫자버튼이 눌릴때마다 어떤 번호가 눌려젔는지를 다이얼에 전달하면(1*), 다이얼에서는 화면에 눌려진 번호의 정보를 띄우면서(1.1), 해당 버튼에 맞는 음을 출력할것이다(1.2).
그리고 전송버튼을 누르면(2) 무선 통신 모듈(radio)에 연결 요청을 보내며(2.1), 연결 여부에 대한 결과를 화면으로 출력할것이다.(A1)
이러한 절차를 다이어그램으로 표시하면 아래와 같다.
정확하진 않겠지만 이를 코드로 표현하면 대략 아래와 같이 나올것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
#include<iostream>
#include<string>
using namespace std;
class Speaker {
public:
void tone(int n) {
cout << "sound : " << n << endl;
}
};
class Screen {
public:
void displayDigit(int n) {
cout << "display : " << n << endl;
}
void inUse() {
cout << "연결 완료" << endl;
}
};
class Radio {
public:
Screen* screen;
Radio(Screen* screen) : screen(screen) {}
Radio() : screen(nullptr) {}
~Radio() {
delete screen;
}
void connect(string pno) {
cout << pno << "번호로 연결 시도" << endl;
// A1 : inUse
screen->inUse();
}
};
class Dialer {
public:
Speaker* speaker;
Screen* screen;
Radio* radio;
string nums;
Dialer(Speaker* speaker, Screen* screen, Radio* radio)
: speaker(speaker), screen(screen), radio(radio), nums("") {}
Dialer() : Dialer(nullptr, nullptr, nullptr) {}
~Dialer() {
delete speaker, delete screen;
}
void digit(int n) {
// 1.1 : displayDigit(n)
screen->displayDigit(n);
// 1.2 : tone(n)
speaker->tone(n);
nums.push_back(n + '0');
}
void Send() {
radio->connect(nums);
nums.clear();
}
void Display(int n) {
screen->displayDigit(n);
}
void Connect() {
radio->connect(nums);
}
};
class Button {
public:
Dialer *dialer;
int n;
Button(Dialer* dialer, int n) : dialer(dialer), n(n) {}
Button() : Button(nullptr, -1) {}
~Button() {
delete dialer;
}
virtual void pressButton() {
// 1* : digit(n)
dialer->digit(n);
}
};
class send : public Button {
public:
send(Dialer* dialer, int n) : Button(dialer, n) {}
send() : Button() {}
~send() {}
void pressButton() override {
// 2 : Send
dialer->Connect();
}
};
int main(void) {
Speaker* tmpSpeaker = new Speaker;
Screen* tmpScreen = new Screen;
Radio* tmpRadio = new Radio(tmpScreen);
// [ : Button ]
Dialer* tmpDialer = new Dialer(tmpSpeaker, tmpScreen, tmpRadio);
Button buttons[10] = {
Button(tmpDialer, 0),
Button(tmpDialer, 1),
Button(tmpDialer, 2),
Button(tmpDialer, 3),
Button(tmpDialer, 4),
Button(tmpDialer, 5),
Button(tmpDialer, 6),
Button(tmpDialer, 7),
Button(tmpDialer, 8),
Button(tmpDialer, 9)};
buttons[0].pressButton();
buttons[1].pressButton();
buttons[0].pressButton();
buttons[1].pressButton();
buttons[2].pressButton();
buttons[3].pressButton();
buttons[4].pressButton();
buttons[5].pressButton();
buttons[6].pressButton();
buttons[7].pressButton();
buttons[8].pressButton();
// [ send : Button ]
Button* sender = new send(tmpDialer, 10);
sender->pressButton();
return 0;
}
|
cs |
점검
위 다이어그램과 코드를 분석해보고 개선할점을 찾아보자.
우선 방금 그린 sequence diagram을 기반으로 class diagram을 그려보자
위 diagram을 통해 의존관계를 확인할 수 있다.
확인결과 Button이 Dialer에 의존해야함을 알 수 있다. 이는 Button 클래스 내에 Dialer를 포함하는 형태가 되며 이는 적절치 못한 설계이다. 우리는 가급적 Button의 소스코드가 Dialer의 소스코드를 언급하지 않도록 하고싶다.
Button은 dialer에 눌러진 버튼의 정보를 받아 핸드폰 내의 여러 모듈에 전송하는 것 이외에 여러 역할을 가진다. (전화기의 ON/OFF 스위치나 메뉴 버튼, 다른 제어 버튼을 제어...) 만약 위 코드처럼 button이 눌려질때마다 dialer의 함수를 호출하는것과 같은 형태를 띄고있다면 다른 용도에 이 Button의 코드를 재사용하지 못한다. 실제로 우리는 숫자 버튼 이외에, send 버튼을 구현을 button을 상속받는 방식으로 구현했는데, 모든 종류의 버튼에 대해 이런식으로 디자인하기에는 상당히 번거로울 것이다.
- 버튼은 사용자가 휴대전화를 제어하는 유일한 입력장치다.
- 휴대전화내 모든 모듈은 휴대전화 버튼의 입력에 맞추어 작동한다.
- 버튼과 모듈은 서로의 존재에 대해 몰라야 하며, 독립적으로 작동해야한다.
즉, Button에서 Dialer에 명령을 전송하는 이 방식을 탈피해야만 한다 그러므로 위 규칙을 준수하기 위해 아래와 같은 방식으로 설계하여 Button을 Dialer에서 분리시키자.
이제 button은 dialer의 정체에 대해 몰라도 된다. 버튼을 눌러질대 마다 그저 ButtonListener의 buttonPressed(token) 함수를 호출하기만 하면된다. button은 이로인해 어떤 일이 일어날지에 대해 신경안써도된다.
Button - ButtonListener - Dialer의 관계를 코드로 표현해보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class ButtonListener{
public:
virtual void buttonPressed(int token) = 0;
};
class Button{
public:
ButtonListener *buttonListner;
int token; // 각 버튼의 식별자
void buttonPressed(){
buttonListner->buttonPressed();
}
};
class Dialer : ButtonListener{
public:
void buttonPressed(int token) override {
switch(token){
// token값에 맞추어 적절한 명령 실행
}
}
};
|
cs |
Button은 Dialer에 대해 몰라도 되지만, Dialer는 각 버튼의 token에 대해 알아야 명령을 실행할 수 있게 되었다.
즉, Dialer는 여전히 Button을 의식해야만 한다.
개선
그러면 Button과 Dialer 사이에 이 둘을 연동하기 위한 Adapter 인스턴스를 하나 둔다.
Adapter하나는 버튼 하나와 대응되며, Button과 Dialer는 Adapter에 대해 모르지만, Adapter는 나머지 둘에 대해 알도록 설계하면 될것이다.
각 Adapter는 digit 필드를 통해 자기자신이 어떤 버튼과 대응되는지 알 수 있다
위 class diagram을 기반으로, dynamic diagram인 sequence diagram을 다시 만들어보자
위 sequence diagram을 코드로 표현하면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
class Screen {
public:
void displayDigit(int n) { /* ... */ }
void inUse();
};
class Speaker {
public:
void tone(int n) { /* ... */ }
};
class Radio {
private:
Screen* screen;
public:
void connect(std::string num) { /* ... */ screen->inUse(); }
};
class Dialer {
private:
Screen* screen;
Speaker* speaker;
Radio* radio;
std::string num;
public:
void displayDigit(int n) { screen->displayDigit(n); }
void tone(int n) { speaker->tone(n); }
void connect() { radio->connect(num); }
void digit(int n) {
num.push_back(n + '0');
}
void Send() { radio->connect(num); }
};
class ButtonListener {
protected:
Dialer* dialer;
public:
virtual void buttonPressed(int token) = 0;
};
// send 버튼에 대한 adapter
class SendButtonDialerAdapter : public ButtonListener {
public:
void buttonPressed(int token) override {
dialer->Send();
}
};
// 0~9 버튼에 대한 adapter
class ButtonDialerAdapter : public ButtonListener {
public:
void buttonPressed(int token) override {
dialer->digit(token);
}
};
class Button {
private:
int token; // 버튼 식별자
public:
ButtonListener *adapter;
void buttonPressed() { adapter->buttonPressed(token); }
};
int main(void) {
Button numButtons[10];
Button sendButton;
/* ... */
return 0;
}
|
cs |
아마 완벽하지는 않을것이라 생각한다.
살펴보면 UML diagram이 코드의 모든 정보를 다 담고있지 않은것을 볼 수 있다.
UML diagram 한장에 너무 많은 세부사항을 그려넣으려 하지 마라. 꼭 필요한 분량 만큼의 세부사항만 사용하는것이 중요하다. 너무 많은 정보를 담으려 하면 오히려 생산성에 해가 된다.
UML diagram은 소스코드가 아니며, 따라서 모든 메서드나 변수, 관계를 선언하는 장소로 취급해서는 안 된다.
문서화
의사소통, 유지보수, 미래의 자기자신을 위해 등 다양한 목적을 위해서라도 프로젝트의 문서화는 필요하다.
좋은 문서가 없으면 팀원들은 코드의 바다에서 헤멜것이며, 반대로 너무 과한 문서는 주의를 분산시키게된다.
- 문서화가 필요한 대상 : 통신 프로토콜, 관계형 DB의 스키마, 재사용 가능한 프레임 워크...
하지만 코드의 모든 내용을 문서화 시키려 해선 안된다. 문서는 반드시 짧고 핵심을 찔러야 한다.
가령 백만줄의 자바 코드로 된 프로젝트에 12명이 일하는 팀이라면, 모두 합쳐 25쪽에서 200쪽 사이의 영구 문서로 충분할것이며, 가능한 짧고 간결하게 만들어야 한다.
- 문서 내용 : UML 다이어그램, 관계형 스키마의 ER 다이어그램, 한쪽이나 두 쪽 분량의 시스템을 빌드하는 방법, 테스트 방법 설명, 소스코드 컨트롤 방법 설명 등
결론
UML은 도구일 뿐, 그 자체가 목적이 되어서는 안된다.
남용하지 말고 절제하는 마음으로 활용할것
'소프트웨어 공학 > uml' 카테고리의 다른 글
클래스 다이어그램 개요 (0) | 2020.07.10 |
---|---|
개요 (0) | 2020.04.25 |