안녕하세요. N` 입니당.
이번 포스팅 에서는 드디어 길고 길었던 "패턴이야기"의 마지막을 장식합니다.
마지막으로 다루는 패턴은 "컴파운드 패턴 (Compound Pattern)" 으로, 이 패턴은 여태까지 배웠던 패턴들의 종합이라 볼 수 있습니다.
컴파운드 패턴은
일반적으로 자주 생길 수 있는 문제를 해결하기 위한 용도로,
2개 이상의 패턴을 결합해서 사용하는 것
을 의미합니다.
즉, 이 패턴의 사용한다는 것은 여태까지 배웠던 여러 패턴들을 결합해서 특정 문제를 해결하는 것을 의미하며, 대표적으로 꽤 유명한 패턴 중 하나인 MVC(Model-View-Controller) 패턴은 이러한 컴파운드 패턴에 속합니다.
오늘 리뷰는 요구사항을 살펴보며 그에 따라 여러 패턴을 사용하게 되는 예제와 함께 MVC 패턴 내부의 컴파운드 원리를 간략하게 알아보려 합니다.
(사용하는 예제 및 자료는 제게 OOP 를 가르쳐 주셨던 한국기술교육대 김상진 교수님의 자료를 참고하였습니다.)
1. 오리꽥꽥 시뮬레이터!
요구사항은 간단합니다.
여러가지 특징을 가진 오리들을 제작하고, 오리들이 꽥꽥하며 우는 시뮬레이터 하는 것이죠.
여러 오리를 제작하기 위해, 첫 번째로 설계한 UML 은 아래와 같습니다. (코드 대신 UML 로 대치...)
이 UML 에 따라 제작된 클래스를 시뮬레이션 하는 프로그램을 저는 다음과 같이 제작하였습니다.
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 |
/**
* 오리 시뮬레이터 정의.
*
* Created by Doohyun on 2017. 11. 19..
*/
public class DuckSimulator {
/**
* 테스트 클래스 제작.
*
* @param args
*/
public static void main(String[] args) {
DuckSimulator duckSimulator = new DuckSimulator();
duckSimulator.simulate();
}
/**
* 시뮬레이터 작성.
*
* <pre>
* 이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
* </pre>
*/
public void simulate(){
Duck rubberDuck = new RubberDuck();
simulate(rubberDuck);
}
/**
* 오리를 넣어 시뮬레이션.
*
* @param duck
*/
private void simulate(Duck duck){
duck.quack();
}
}
// CONSOLE RESULT
// 삐이익~~
|
cs |
2. 오리가 아닌 다른 동물을 테스트!
테스트 클래스는 현재 잘 작동하고 있습니다만, 오리와 비슷한 성격을 가진 거위 클래스를 이 테스트에 포함하고 싶습니다.
거위 클래스 명세는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
/**
* 거위 클래스
*
* Created by Doohyun on 2017. 11. 19..
*/
public class Goose {
/**
* 거위의 울음.
*/
public void honk() {
System.out.println("Honk");
}
}
|
cs |
해당 테스트 클래스에서 거위를 사용하려면, 오리와 호환을 맞춰야할 것으로 보입니다.
좋은 방법이 있을까요? 여러분은 알고 있을 것이라 생각합니다.
아래 패턴을 기억해 보죠.
제작한 거위 어댑터는 아래와 같습니다.
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 |
/**
* 거위를 오리로 호환시켜주는 클래스
*
* Created by Doohyun on 2017. 11. 19..
*/
public class GooseAdapter extends Duck {
private Goose goose;
public GooseAdapter(Goose goose) {
this.goose = goose;
}
@Override
public void quack() {
goose.honk();
}
}
// TEST CLASS
public class DuckSimulator {
/**
* 시뮬레이터 작성.
*
* <pre>
* 이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
* </pre>
*/
public void simulate(){
// 거위를 오리로 호환하여, 테스트
Duck goose = new GooseAdapter(new Goose());
simulate(goose);
}
}
// CONSOLE RESULT
// Honk
|
cs |
3. 오리들의 꽥꽥한 횟수 구하기!
추가된 요구사항은 시뮬레이션을 한번 할 때, 전체 오리들이 꽥꽥한 횟수를 구해야하는 것입니다.
이를 구현하기 위한 방법으로 static 변수를 이용하고자 합니다.
구현 알고리즘으로 떠오르는 것은 quack 메소드가 호출될 때마다 해당 변수를 증가시키면 되지만, 이를 위해서는 Duck 을 구현한 모든 클래스를 손봐야하는 것으로 보입니다. (많이 고쳐야 함.. 짜증...)
(Duck::quack 자체를 추상 메소드에서 일반 메소드로 변경하고 카운트를 증가시켜도 되지만, 이를 위해서는 상속받은 모든 오리클래스들이 quack 을 재정의할 때 super.quack 을 반드시 사용하도록 모두 고쳐야 합니다.)
이를 위해서 제안하는 방법은 꽥꽥한 횟수만을 관리하는 클래스를 제공하는 것을 목표로 합니다.
요구사항인 Duck::quack 을 요청했을 때 카운트가 증가해야하는 재정의가 필요하며, 이는 편의에 맞게 상속을 우회하는 패턴을 생각해 볼 수 있습니다.
우리는 이런 방법론을 배운적이 있습니다.
그렇습니다. 횟수를 관리하는 장식자를 만들 것입니다.
저는 아래와 같이 장식자를 제작하였고, 그에 따라 시뮬레이터 역시 변경하였습니다.
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 |
/**
* 꽥꽥한 횟수를 관리하는 장식자
*
* Created by Doohyun on 2017. 11. 19..
*/
public class QuackCountDecorator extends Duck{
private Duck duck;
private static int QUACK_COUNT = 0;
public QuackCountDecorator(Duck duck) {
this.duck = duck;
}
@Override
public void quack() {
// 오리가 울 때마다, 카운트를 관리하도록 처리.
++QUACK_COUNT;
duck.quack();
}
/**
* 오리가 꽥꽥한 횟수 출력.
*
* @return
*/
public static int GetQuackCount() {
return QUACK_COUNT;
}
}
/**
* 오리 시뮬레이터 정의.
*
* Created by Doohyun on 2017. 11. 19..
*/
public class DuckSimulator {
/**
* 시뮬레이터 작성.
*
* <pre>
* 이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
* </pre>
*/
public void simulate(){
// 고무오리 테스트.
{
Duck rubberDuck = new QuackCountDecorator(new RubberDuck());
simulate(rubberDuck);
}
// 거위를 오리로 호환하여, 테스트
{
// 장식자로 치장.
Duck goose = new QuackCountDecorator(new GooseAdapter(new Goose()));
simulate(goose);
}
System.out.println(String.format("꽥꽥 횟수 출력 : %d회", QuackCountDecorator.GetQuackCount()));
}
}
// CONSOLE RESULT
// 삐이익~~
// Honk
// 꽥꽥 횟수 출력 : 2회
|
cs |
4. 장식자 생성의 불편함, 이를 위한 객체 생성의 캡슐화!
장식자를 이용하여, 꽥꽥 횟수에 대한 관심을 한 곳으로 몰아 버린 것은 적절한 선택인 듯 합니다.
하지만, 그에 대한 비용으로 불편한 API 사용이란 대가를 치뤄야 합니다.
대표적으로 주목할 코드는 아래의 어댑터와 장식자를 같이 사용한 사례입니다.
|
Duck goose = new QuackCountDecorator(new GooseAdapter(new Goose())); |
cs |
카운트를 모든 오리에 대해서 관리해야 한다면, 매번 위와 같이 장식자를 붙여줘야 할 것으로 보이는데요.
차라리, 해당 방식으로 객체를 생성해야할 일이 많다면 객체생성 방법 자체를 캡슐화하는 것도 방법으로 보입니다.
특정 목적에 맞게 객체를 생산해주는 클래스를 팩토리라 하며, 카운트에 대한 관심을 지원할 수 있는 팩토리 클래스를 제작해 봅시다.
(이에 대한 자세한 내용은 아래에서 참고!)
이 예제에서는 여러 타입의 객체를 목적에 맞게 생산해주는 추상 팩토리(Abstract Factory) 를 사용하였으며,
그에 대한 코드는 아래와 같습니다.
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 |
/**
* 카운트 뿐만이 아닌 특정 관심사에 따라 팩토리를 생성할 수 있음으로, 추상 팩토리를 제작.
*
* Created by Doohyun on 2017. 11. 19..
*/
public abstract class AbstractDuckFactory {
/**
* 고무오리 생산.
*
* @return
*/
public abstract Duck createRubberDuck();
/**
* 거위 생산.
*
* @return
*/
public abstract Duck createGoose();
}
/**
* 꽥꽥 카운팅 데코레이터를 장식한 오리 객체를 생산하는 팩토리.
*
* Created by Doohyun on 2017. 11. 19..
*/
public class CountingDuckFactory extends AbstractDuckFactory{
private static CountingDuckFactory INSTANCE = new CountingDuckFactory();
private CountingDuckFactory(){}
/**
* 싱글톤으로 제작.
*
* <pre>
* 팩토리는 의미적으로 한개여도 충분해 보임.
* </pre>
*
* @return
*/
public static CountingDuckFactory GetInstance() {
return INSTANCE;
}
/**
* 고무오리 생산.
*
* @return
*/
@Override
public Duck createRubberDuck() {
// QuackCountDecorator 으로 장식하였음!
return new QuackCountDecorator(new RubberDuck());
}
/**
* 거위 생산.
*
* @return
*/
@Override
public Duck createGoose() {
return new QuackCountDecorator(new GooseAdapter(new Goose()));
}
}
|
cs |
현재까지의 요구사항에서는 AbstractDuckFactory 를 굳이 제작할 필요는 없어보이지만,
추 후 오리들에 대한 새로운 관심사가 생길 경우 그에 맞는 적절한 객체를 생성할 수 있도록 추상 클래스를 제작했습니다.
또한 구현된 CountingDuckFactory 는 현재 의미적으로 한개여도 충분해 보인다는 생각이 들며, 그에 따라 싱글톤 클래스로 제작하였습니다.
팩토리를 제작하였으니, 그에 따라 테스트 클래스는 다음과 같이 편리하게 고칠 수 있을 것 같네요..
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 |
/**
* 오리 시뮬레이터 정의.
*
* Created by Doohyun on 2017. 11. 19..
*/
public class DuckSimulator {
/**
* 시뮬레이터 작성.
*
* <pre>
* 이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
* </pre>
*/
public void simulate(){
// 고무오리 테스트.
{
// 팩토리로 객체 생성을 캡슐화
Duck rubberDuck = CountingDuckFactory.GetInstance().createRubberDuck();
simulate(rubberDuck);
}
// 거위를 오리로 호환하여, 테스트
{
// 팩토리로 객체 생성을 캡슐화
Duck goose = CountingDuckFactory.GetInstance().createGoose();
simulate(goose);
}
System.out.println(String.format("꽥꽥 횟수 출력 : %d회", QuackCountDecorator.GetQuackCount()));
}
}
|
cs |
5. 오리떼 관찰!
현재 제작된 오리 시뮬레이터에서는 각각의 오리객체가 꽥꽥하도록 정의되어 있습니다.
하지만 오리 객체를 더 많이 테스트 하고 싶다면, 팩토리에서 객체를 생성하고 DuckSimulator::simulate 메소드를 매번 호출해야 합니다.
(즉 여러 오리 객체에 대한 관리가 불편합니다. ㅡㅡ^)
만약 테스트의 요구사항이 지금과 같이 여러 오리들을 순회하며 Duck::quack 을 실행하는 것이 목적이라면, 아래 패턴을 이용해 볼 수 있을 것 같습니다.
지금부터 오리떼(Flock)를 나타낼 수 있는 클래스의 제작을 목표로 하며, DuckSimulator::simulate 를 사용할 수 있도록 하려 합니다.
이 때 오리들을 순회하면서 동작하고, 기존 Duck 의 인터페이스를 그대로 계승하도록 해야할 것 입니다.
즉, 오리들을 그룹으로 묶은 복합계층(Composite)을 만들고 단일계층(RubberDuck..)과 동일한 인터페이스를 제공할 생각입니다.
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 |
/**
* 오리떼 클래스.
*
* <pre>
* 오리떼라는 복합계층을 제작하며, 단일계층과 동일한 취급을 할 수 있게 해줌.
* Composite Pattern
* </pre>
*
* Created by Doohyun on 2017. 11. 19..
*/
public class Flock extends Duck{
private List<Duck> duckList = new ArrayList<>();
public void addDuck(Duck duck) {
duckList.add(duck);
}
@Override
public void quack() {
// 순회하면서, Duck::quack 기능을 수행.
// 반복자 패턴(Iterator)
duckList.forEach(Duck::quack);
}
}
/**
* 오리 시뮬레이터 정의.
*
* Created by Doohyun on 2017. 11. 19..
*/
public class DuckSimulator {
/**
* 시뮬레이터 작성.
*
* <pre>
* 이 곳에 비지니스 시뮬레이션 기능을 넣을 것!
* </pre>
*/
public void simulate(){
// 오리떼 생성.
Flock flock = new Flock();
// 고무오리 추가.
flock.addDuck(CountingDuckFactory.GetInstance().createRubberDuck());
// 거위 추가
flock.addDuck(CountingDuckFactory.GetInstance().createGoose());
// 오리떼 테스트.
simulate(flock);
}
}
|
cs |
여기까지, 오리 시뮬레이터의 요구사항을 일단 마치려고 합니다.
이 곳에서 사용된 패턴은 Adapter, Decorator, Factory, Composite, Iterator 패턴으로 오리 시뮬레이터를 제작하기 위해 연속적으로 패턴이 사용되었습니다.
위의 언급된 예제에서는 일련의 패턴들이 어떻게 연속적으로 사용될 수 있는지 볼 수 있습니다.
하지만, 이 것이 컴파운드-패턴은 아닙니다.
여러 패턴을 동시에 사용하였다고 컴파운드-패턴은 아니고, 여러 패턴이 결합되어 특정 용도로 사용되는 것을 말하기 때문이죠.
지금부터는 대표적인 컴파운드-패턴인 MVC 를 살짝 보며, MVC 가 어떤 용도로 여러가지 패턴들을 사용하고 있는지 볼 것입니다.
6. MVC
MVC 는 Model-View-Controller 의 약자로 Model(데이터 및 비지니스 문제)과 View(화면) 사이에 제어하는 층(Controller)를 두는 것을 목표로 합니다.
세 가지의 각 컴포넌트는 다음과 같은 역할을 하고 있습니다.
- Model
모든 데이터 및 비지니스 로직들을 포함합니다. 또한 View 와 Controller 에게 본인의 상태(데이터)를 제공할 수 있는 인터페이스 및 상태 변경 이벤트를 제공하지만, 보통은 다른 두 컴포넌트들에게 별 관심은 없습니다.
- View
Model 을 표현할 수 있는 기능을 담당합니다.
데이터를 표현하기 위해 Model 로 부터 상태를 가져오도록 하며, 사용자의 입력을 Controller 로 전달하곤 합니다.
- Controller
View 로부터 사용자의 입력 이벤트를 인지하며, Model 의 상태변경을 요청하거나 View 에게 표시변경을 요청합니다.
이 세 가지 컴포넌트를 패턴 관점에서 보면, 보편적으로 다음과 같이 나누곤 합니다.
MVC 의 목적은 Model 과 View 를 분리하는 것으로 이는 꽤 중요한 이슈입니다.
이 원칙을 지킬 시, 이를테면 아래와 같은 이득을 볼 수 있습니다.
- 비지니스적인 문제는 일관될 수 있지만, 이를 표현하는 방법은 다양해질 수 있습니다.
CUI -> GUI 로 변경 시, 비지니스 문제를 변경할 필요가 있을지 고민해봐야 합니다.
혹은 일관된 데이터로 여러 뷰를 표현해야 할 수도 있죠.
- 표현하기 위한 방법이 분리되어 있다면,
비지니스 문제가 변경되도 표현방법만 알맞게 호환해주면 원활하게 개발할 수 있습니다.
이 것으로, Effective OOP 카테고리도 마무리입니다.
올해부터 시작한 OOP 에 대한 정리가 끝나게 되니, 정말 감회가 새로운 것 같네요. :-)
여기까지 정리를 할 수 있었고, 공부를 할 수 있게 해준 것은 꾸준히 제 블로그를 봐주고 스터디에 적극적으로 참여해주신 구성원분들 덕이라고 생각을 합니다.
(잊지 못할 것입니다. ㅠㅡㅠ)
또한, 이러한 기반 지식 및 자료를 제게 알려주신 교수님께도 감사합니다. ㅎㅎ
여기까지 공부를 마쳤다면, 저를 포함한 이 글을 읽는 모든 분들은 이 책을 한 권 마스터한 것입니다.
나름, 수준있는 예제를 들고왔기 때문에 이 책을 보며, 블로그를 다시 읽어주신다면 정말로 도움이 되지 않을까 생각이 드네요.
(여러분들도 이제 패턴병에 걸릴 시간입니다. 히히히)
조금 부족한 면이 없는 글들이긴 하지만,
이 글들을 읽은 모든 분들이 정말 멋있고 훌륭한 개발자가 되는 것에 조금이라도 도움이 되길 바랍니다. ^^