LostCatBox

(ETC) 디자인 패턴에 대해서 CH2 (구조 디자인 패턴에 대해)

Word count: 5.7kReading time: 35 min
2023/03/21 Share

구조 패턴

어댑터 패턴

의도

어댑터는 호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 하는 구조적 디자인 패턴입니다.

스크린샷 2023-03-21 오후 10.29.40

어댑터는 호출을 받으면 들어오는 XML 데이터를 JSON 구조로 변환한 후 해당 호출을 래핑된 분석 객체의 적절한 메서드들에 전달합니다.

객체 어댑터

이 구현은 객체 합성 원칙을 사용합니다. 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑합니다.

스크린샷 2023-03-21 오후 10.30.27

  1. 클라이언트는 프로그램의 기존 비즈니스 로직을 포함하는 클래스입니다.
  2. 클라이언트 인터페이스는 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜을 뜻합니다.
  3. 서비스는 일반적으로 타사 또는 레거시의 유용한 클래스를 뜻합니다. 클라이언트는 서비스 클래스를 직접 사용할 수 없습니다. 왜냐하면 서비스 클래스는 호환되지 않는 인터페이스를 가지고 있기 때문입니다.
  4. 어댑터는 클라이언트와 서비스 양쪽에서 작동할 수 있는 클래스로, 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현합니다. 어댑터는 어댑터 인터페이스를 통해 클라이언트로부터 호출들을 수신한 후 이 호출을 래핑된 서비스 객체가 이해할 수 있는 형식의 호출들로 변환합니다.
  5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구상 어댑터 클래스와 결합하지 않습니다. 덕분에 기존 클라이언트 코드를 손상하지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있습니다. 이것은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용할 수 있습니다: 클라이언트 코드를 변경하지 않은 채 새 어댑터 클래스를 생성할 수 있으니까요.

클래스 어댑터( 다중 상속을 지원하는 프로그래밍 언어에서만 구현 가능)
이 구현은 상속을 사용하며, 어댑터는 동시에 두 객체의 인터페이스를 상속합니다. 이 방식은 C++ 와 같이 다중 상속을 지원하는 프로그래밍 언어에서만 구현할 수 있습니다.

  • 클래스 어댑터는 객체를 래핑할 필요가 없습니다. 그 이유는 클라이언트와 서비스 양쪽에서 행동들을 상속받기 때문입니다. 위의 어댑테이션(적용)은 오버라이딩된 메서드 내에서 발생합니다. 위 어댑터는 기존 클라이언트 클래스 대신 사용할 수 있습니다.
  • 스크린샷 2023-03-21 오후 10.31.46

예시

스크린샷 2023-03-21 오후 10.32.10

어댑터는 정사각형 지름의 절반(즉, 사각형 못을 수용할 수 있는 가장 작은 원의 반지름)을 반지름으로 가진 둥근 못인 척 합니다.

결국 RoundHole에서 fits(RoundPeg roundPeg)함수를 처리해야한다. 이는 매개변수로 RoundPeg 타입이 필요하다.

SquarePeg가 RoundPeg타입으로 작용할려면 RoundPeg를 상속받은 SquarePegAdapter를 사용하여, fits()함수의 매개변수로 활용될 수 있다.

1
2
3
4
5
6
7
8
9
SquarePeg smallSqPeg = new SquarePeg(2);
// hole.fits(smallSqPeg); // Won't compile.

// Adapter solves the problem.
SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg)

if (hole.fits(smallSqPegAdapter)) {
System.out.println("Square peg w2 fits round hole r5.");
}

전체 예시 코드

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
public class RoundHole {
private double radius;

public RoundHole(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}

public boolean fits(RoundPeg peg) {
boolean result;
result = (this.getRadius() >= peg.getRadius());
return result;
}
}

public class RoundPeg {
private double radius;

public RoundPeg() {}

public RoundPeg(double radius) {
this.radius = radius;
}

public double getRadius() {
return radius;
}
}

/**
* Adapter allows fitting square pegs into round holes.
* 어댑터는 정사각형 지름의 절반(즉, 사각형 못을 수용할 수 있는 가장 작은 원의 반지름)을 반지름으로 가진 둥근 못인 척 합니다.
*/
public class SquarePegAdapter extends RoundPeg {
private SquarePeg peg;

public SquarePegAdapter(SquarePeg peg) {
this.peg = peg;
}

@Override
public double getRadius() {
double result;
// Calculate a minimum circle radius, which can fit this peg.
result = (Math.sqrt(Math.pow((peg.getWidth() / 2), 2) * 2));
return result;
}
}


public class SquarePeg {
private double width;

public SquarePeg(double width) {
this.width = width;
}

public double getWidth() {
return width;
}

public double getSquare() {
double result;
result = Math.pow(this.width, 2);
return result;
}
}


@GetMapping("/designpattern6")
@ResponseBody
public HttpStatus designpattern6(HttpServletRequest request) {
System.out.println(request.getRequestURL());

// Round fits round, no surprise.
RoundHole hole = new RoundHole(5);
RoundPeg rpeg = new RoundPeg(5);
if (hole.fits(rpeg)) {
System.out.println("Round peg r5 fits round hole r5.");
}

SquarePeg smallSqPeg = new SquarePeg(2);
SquarePeg largeSqPeg = new SquarePeg(20);
// hole.fits(smallSqPeg); // Won't compile.

// Adapter solves the problem.
SquarePegAdapter smallSqPegAdapter = new SquarePegAdapter(smallSqPeg);
SquarePegAdapter largeSqPegAdapter = new SquarePegAdapter(largeSqPeg);
if (hole.fits(smallSqPegAdapter)) {
System.out.println("Square peg w2 fits round hole r5.");
}
if (!hole.fits(largeSqPegAdapter)) {
System.out.println("Square peg w20 does not fit into round hole r5.");
}

return HttpStatus.OK;
}

적용

어댑터 클래스는 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용하세요.

어댑터 패턴은 당신의 코드와 레거시 클래스, 타사 클래스 또는 특이한 인터페이스가 있는 다른 클래스 간의 변환기 역할을 하는 중간 레이어 클래스를 만들 수 있도록 합니다.

이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용하세요.

각 자식 클래스를 확장한 후 누락된 기능들을 새 자식 클래스들에 넣을 수 있습니다. 하지만 해당 코드를 모든 새 클래스들에 복제해야 하며, 그건 정말 나쁜 냄새가 나는 코드일 것입니다.

이보다 훨씬 더 깔끔한 해결책은 누락된 기능을 어댑터 클래스에 넣는 것입니다. 그 후 어댑터 내부에 누락된 기능이 있는 객체들을 래핑하면 필요한 기능들을 동적으로 얻을 것입니다. 이 해결책이 작동하려면 대상 클래스들에는 반드시 공통 인터페이스가 있어야 하며 어댑터의 필드는 해당 인터페이스를 따라야 합니다. 위 접근 방식은 데코레이터 패턴과 매우 유사합니다.

장단점

장점

  • 단일 책임 원칙. 프로그램의 기본 비즈니스 로직에서 인터페이스 또는 데이터 변환 코드를 분리할 수 있습니다.
  • 개방/폐쇄 원칙. 클라이언트 코드가 클라이언트 인터페이스를 통해 어댑터와 작동하는 한, 기존의 클라이언트 코드를 손상시키지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있습니다.

단점

  • 다수의 새로운 인터페이스와 클래스들을 도입해야 하므로 코드의 전반적인 복잡성이 증가합니다. 때로는 코드의 나머지 부분과 작동하도록 서비스 클래스를 변경하는 것이 더 간단합니다.

브리지 패턴

의도

브리지는 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴입니다.

즉, 추상화한후, 객체 생성시 관련 의존하는 객체도 생성자에서 결정하게함.

스크린샷 2023-03-23 오후 10.10.29

구조

스크린샷 2023-03-23 오후 10.10.56

  1. 추상화는 상위 수준의 제어 논리를 제공하며, 구현 객체에 의존해 실제 하위 수준 작업들을 수행합니다.

  2. 구현은 모든 구상 구현들에 공통적인 인터페이스를 선언하며, 추상화는 여기에 선언된 메서드들을 통해서만 구현 객체와 소통할 수 있습니다.

    추상화는 구현과 같은 메서드들을 나열할 수 있지만 보통은 구현이 선언한 다양한 원시 작업들에 의존하는 몇 가지 복잡한 행동들을 선언합니다.

  3. 구상 구현들에는 플랫폼별 맞춤형 코드가 포함됩니다.

  4. 정제된 추상화들은 제어 논리의 변형들을 제공합니다. 그들은 그들의 부모처럼 일반 구현 인터페이스를 통해 다른 구현들과 작업합니다.

  5. 일반적으로 클라이언트는 추상화와 작업하는데만 관심이 있습니다. 그러나 추상화 객체를 구현 객체들 중 하나와 연결하는 것도 클라이언트의 역할입니다.

예시

리모콘과 디바이스

스크린샷 2023-03-23 오후 10.11.50

기초 리모컨 클래스는 이 클래스를 장치 객체와 연결하는 참조 필드를 선언합니다. 모든 리모컨은 일반 장치 인터페이스를 통해 장치들과 작동하므로 같은 리모컨이 여러 장치 유형을 지원할 수 있습니다.

장치 클래스들과 독립적으로 리모컨 클래스들을 개발할 수 있으며, 필요한 것은 새로운 리모컨 자식 클래스를 만드는 것뿐입니다. 예를 들어 기초 리모컨에는 버튼이 두 개뿐일 수 있지만, 추가 터치스크린과 추가 배터리 같은 기능들도 가지도록 확장할 수 있습니다.

클라이언트 코드는 Remote의 생성자를 통해 원하는 유형의 리모컨을 특정 장치 객체와 연결합니다.

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
public interface Device {
boolean isEnabled();

void enable();

void disable();

int getVolume();

void setVolume(int percent);

int getChannel();

void setChannel(int channel);

void printStatus();
}


public class Radio implements Device {
private boolean on = false;
private int volume = 30;
private int channel = 1;

@Override
public boolean isEnabled() {
return on;
}

@Override
public void enable() {
on = true;
}

@Override
public void disable() {
on = false;
}

@Override
public int getVolume() {
return volume;
}

@Override
public void setVolume(int volume) {
if (volume > 100) {
this.volume = 100;
} else if (volume < 0) {
this.volume = 0;
} else {
this.volume = volume;
}
}

@Override
public int getChannel() {
return channel;
}

@Override
public void setChannel(int channel) {
this.channel = channel;
}

@Override
public void printStatus() {
System.out.println("------------------------------------");
System.out.println("| I'm radio.");
System.out.println("| I'm " + (on ? "enabled" : "disabled"));
System.out.println("| Current volume is " + volume + "%");
System.out.println("| Current channel is " + channel);
System.out.println("------------------------------------\n");
}
}

public class Tv implements Device {
private boolean on = false;
private int volume = 30;
private int channel = 1;

@Override
public boolean isEnabled() {
return on;
}

@Override
public void enable() {
on = true;
}

@Override
public void disable() {
on = false;
}

@Override
public int getVolume() {
return volume;
}

@Override
public void setVolume(int volume) {
if (volume > 100) {
this.volume = 100;
} else if (volume < 0) {
this.volume = 0;
} else {
this.volume = volume;
}
}

@Override
public int getChannel() {
return channel;
}

@Override
public void setChannel(int channel) {
this.channel = channel;
}

@Override
public void printStatus() {
System.out.println("------------------------------------");
System.out.println("| I'm TV set.");
System.out.println("| I'm " + (on ? "enabled" : "disabled"));
System.out.println("| Current volume is " + volume + "%");
System.out.println("| Current channel is " + channel);
System.out.println("------------------------------------\n");
}
}


public interface Remote {
void power();

void volumeDown();

void volumeUp();

void channelDown();

void channelUp();
}


import refactoring_guru.bridge.example.devices.Device;

public class BasicRemote implements Remote {
protected Device device;

public BasicRemote() {}

public BasicRemote(Device device) {
this.device = device;
}

@Override
public void power() {
System.out.println("Remote: power toggle");
if (device.isEnabled()) {
device.disable();
} else {
device.enable();
}
}

@Override
public void volumeDown() {
System.out.println("Remote: volume down");
device.setVolume(device.getVolume() - 10);
}

@Override
public void volumeUp() {
System.out.println("Remote: volume up");
device.setVolume(device.getVolume() + 10);
}

@Override
public void channelDown() {
System.out.println("Remote: channel down");
device.setChannel(device.getChannel() - 1);
}

@Override
public void channelUp() {
System.out.println("Remote: channel up");
device.setChannel(device.getChannel() + 1);
}
}

import refactoring_guru.bridge.example.devices.Device;

public class AdvancedRemote extends BasicRemote {

public AdvancedRemote(Device device) {
super.device = device;
}

public void mute() {
System.out.println("Remote: mute");
device.setVolume(0);
}
}


public class Demo {
public static void main(String[] args) {
testDevice(new Tv());
testDevice(new Radio());
}

public static void testDevice(Device device) {
System.out.println("Tests with basic remote.");
BasicRemote basicRemote = new BasicRemote(device);
basicRemote.power();
device.printStatus();

System.out.println("Tests with advanced remote.");
AdvancedRemote advancedRemote = new AdvancedRemote(device);
advancedRemote.power();
advancedRemote.mute();
device.printStatus();
}
}
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
Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm TV set.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------

Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm TV set.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------

Tests with basic remote.
Remote: power toggle
------------------------------------
| I'm radio.
| I'm enabled
| Current volume is 30%
| Current channel is 1
------------------------------------

Tests with advanced remote.
Remote: power toggle
Remote: mute
------------------------------------
| I'm radio.
| I'm disabled
| Current volume is 0%
| Current channel is 1
------------------------------------

적용

브리지 패턴은 당신이 어떤 기능의 여러 변형을 가진 모놀리식 클래스를 나누고 정돈하려 할 때 사용하세요. (예: 클래스가 다양한 데이터베이스 서버들과 작동할 수 있는 경우).

클래스가 성장할수록 그 작동 방식을 파악하기가 더 어려워지고 해당 클래스를 변경하는 데 더더욱 오랜 시간이 걸립니다. 클래스 기능의 여러 변형 중 하나를 변경하려면 클래스 전체에 걸쳐 여러 가지 변경을 수행해야 할 수 있으며, 이를 수행 중 개발자들은 종종 실수하거나 일부 중요한 부작용들을 해결하지 않기도 합니다.

브리지 패턴을 사용하면 모놀리식 클래스를 여러 클래스 계층구조로 나눌 수 있습니다. 그런 다음 각 계층구조의 클래스들을 다른 계층구조들에 있는 클래스들과는 독립적으로 변경할 수 있습니다. 이 접근 방식은 코드의 유지관리를 단순화하고 기존 코드가 손상될 위험을 최소화합니다.

이 패턴은 여러 직교(독립) 차원에서 클래스를 확장해야 할 때 사용하세요.

브리지 패턴은 각 차원에 대해 별도의 클래스 계층구조를 추출할 것을 제안합니다. 원래 클래스는 모든 작업을 자체적으로 수행하는 대신 추출된 계층구조들에 속한 객체들에 관련 작업들을 위임합니다.

브리지 패턴은 런타임(실행시간)에 구현을 전환할 수 있어야 할 때에 사용하세요.

선택 사항이지만 브리지 패턴을 사용하면 추상화 내부의 구현 객체를 바꿀 수 있으며, 그렇게 하려면 필드에 새 값을 할당하기만 하면 됩니다.

위 항목은 많은 사람이 브리지와 전략 패턴을 혼동하는 주된 이유입니다. 패턴은 클래스의 구조를 설계하는 특정 방법 그 이상의 것이라는 것을 기억하세요. 패턴은 제기되고 있는 문제 및 의도에 대하여도 소통할 수 있습니다.

장단점

장점

  • 플랫폼 독립적인 클래스들과 앱들을 만들 수 있습니다.
  • 클라이언트 코드는 상위 수준의 추상화를 통해 작동하며, 플랫폼 세부 정보에 노출되지 않습니다.
  • 개방/폐쇄 원칙. 새로운 추상화들과 구현들을 상호 독립적으로 도입할 수 있습니다.
  • 단일 책임 원칙. 추상화의 상위 수준 논리와 구현의 플랫폼 세부 정보에 집중할 수 있습니다.

단점

  • 결합도가 높은 클래스에 패턴을 적용하여 코드를 더 복잡하게 만들 수 있습니다.

복합체 패턴

의도

복합체 패턴은 객체들을 트리 구조들로 구성한 후, 이러한 구조들과 개별 객체들처럼 작업할 수 있도록 하는 구조 패턴입니다.

구조

스크린샷 2023-03-27 오후 10.16.38

  1. 컴포넌트 인터페이스는 트리의 단순 요소들과 복잡한 요소들 모두에 공통적인 작업을 설명합니다.(잎, 컨테이너 둘다 가져야하는 공통적인 작업)

  2. 은 트리의 기본 요소이며 하위요소가 없습니다.

    일반적으로 잎 컴포넌트들은 작업을 위임할 하위요소가 없어서 대부분의 실제 작업들을 수행합니다.

  3. 컨테이너(일명 복합체)는 하위 요소들(잎 또는 기타 컨테이너)이 있는 요소입니다. 컨테이너는 자녀들의 구상 클래스들을 알지 못하며, 컴포넌트 인터페이스를 통해서만 모든 하위 요소들과 함께 작동합니다.

    요청을 전달받으면 컨테이너는 작업을 하위 요소들에 위임하고 중간 결과들을 처리한 다음 최종 결과들을 클라이언트에 반환합니다.

  4. 클라이언트는 컴포넌트 인터페이스를 통해 모든 요소들과 작동합니다. 그 결과 클라이언트는 트리의 단순 요소들 또는 복잡한 요소들 모두에 대해 같은 방식으로 작업할 수 있습니다.

예시

스크린샷 2023-03-27 오후 10.18.03

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
interface Component {
void operation(); //공통동작
}


class Leaf implements Component {

@Override
public void operation() {
System.out.println(this + " 호출");
}
}


class Composite implements Component {

// Leaf 와 Composite 객체 모두를 저장하여 관리하는 내부 리스트
List<Component> components = new ArrayList<>();

public void add(Component c) {
components.add(c); // 리스트 추가
}

public void remove(Component c) {
components.remove(c); // 리스트 삭제
}

@Override
public void operation() {
System.out.println(this + " 호출");

// 내부 리스트를 순회하여, 단일 Leaf이면 값을 출력하고,
// 또다른 서브 복합 객체이면, 다시 그 내부를 순회하는 재귀 함수 동작이 된다.
for (Component component : components) {
component.operation(); // 자기 자신을 호출(재귀)
}
}

public List<Component> getChild() {
return components;
}
}
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
class Client {
public static void main(String[] args) {
// 1. 최상위 복합체 생성
Composite composite1 = new Composite();

// 2. 최상위 복합체에 저장할 Leaf와 또다른 서브 복합체 생성
Leaf leaf1 = new Leaf();
Composite composite2 = new Composite();

// 3. 최상위 복합체에 개체들을 등록
composite1.add(leaf1);
composite1.add(composite2);

// 4. 서브 복합체에 저장할 Leaf 생성
Leaf leaf2 = new Leaf();
Leaf leaf3 = new Leaf();
Leaf leaf4 = new Leaf();

// 5. 서브 복합체에 개체들을 등록
composite2.add(leaf2);
composite2.add(leaf3);
composite2.add(leaf4);

// 6. 최상위 복합체의 모든 자식 노드들을 출력
composite1.operation();
}
}

적용

복합체 패턴은 나무와 같은 객체 구조를 구현해야 할 때 사용하세요.

복합체 패턴은 공통 인터페이스를 공유하는 두 가지 기본 요소 유형들인 단순 잎들과 복합 컨테이너들을 제공합니다. 컨테이너는 잎들과 다른 컨테이너들로 구성될 수 있으며, 이를 통해 나무와 유사한 중첩된 재귀 객체 구조를 구성할 수 있습니다.

이 패턴은 클라이언트 코드가 단순 요소들과 복합 요소들을 모두 균일하게 처리하도록 하고 싶을 때 사용하세요.

복합체 패턴에 의해 정의된 모든 요소들은 공통 인터페이스를 공유하며, 이 인터페이스를 사용하면 클라이언트는 작업하는 객체들의 구상 클래스에 대해 걱정할 필요가 없습니다.

장단점

장점

  • 다형성과 재귀를 당신에 유리하게 사용해 복잡한 트리 구조들과 더 편리하게 작업할 수 있습니다.
  • 개방/폐쇄 원칙. 객체 트리와 작동하는 기존 코드를 훼손하지 않고 앱에 새로운 요소 유형들을 도입할 수 있습니다.

단점

  • 기능이 너무 다른 클래스들에는 공통 인터페이스를 제공하기 어려울 수 있으며, 어떤 경우에는 컴포넌트 인터페이스를 과도하게 일반화해야 하여 이해하기 어렵게 만들 수 있습니다.

데코레이터 패턴

의도

데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴입니다.

즉, 기본 객체의 행동에 다양한 기능들을 추가해야하는 수가 많은경우, 유리해진다

구조

스크린샷 2023-03-27 오후 10.24.36

  1. 컴포넌트는 래퍼들과 래핑된 객체들 모두에 대한 공통 인터페이스를 선언합니다.
  2. 구상 컴포넌트는 래핑되는 객체들의 클래스이며, 그는 기본 행동들을 정의하고 해당 기본 행동들은 데코레이터들이 변경할 수 있습니다.
  3. 기초 데코레이터 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 구상 컴포넌트들과 구상 데코레이터들을 모두 포함할 수 있도록 컴포넌트 인터페이스로 선언되어야 합니다. 그 후 기초 데코레이터는 모든 작업들을 래핑된 객체에 위임합니다.
  4. 구상 데코레이터들은 컴포넌트들에 동적으로 추가될 수 있는 추가 행동들을 정의합니다. 그들은 기초 데코레이터의 메서드를 오버라이드(재정의)하고 해당 행동을 부모 메서드를 호출하기 전이나 후에 실행합니다.
  5. 클라이언트는 아래에 언급한 데코레이터들이 컴포넌트 인터페이스를 통해 모든 객체와 작동하는 한 컴포넌트들을 여러 계층의 데코레이터들로 래핑할 수 있습니다.

예시

스크린샷 2023-03-27 오후 10.25.23

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
public interface DataSource {
void writeData(String data);

String readData();
}


public class FileDataSource implements DataSource {
private String name;

public FileDataSource(String name) {
this.name = name;
}

@Override
public void writeData(String data) {
File file = new File(name);
try (OutputStream fos = new FileOutputStream(file)) {
fos.write(data.getBytes(), 0, data.length());
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}

@Override
public String readData() {
char[] buffer = null;
File file = new File(name);
try (FileReader reader = new FileReader(file)) {
buffer = new char[(int) file.length()];
reader.read(buffer);
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
return new String(buffer);
}
}



public class DataSourceDecorator implements DataSource {
private DataSource wrappee;

DataSourceDecorator(DataSource source) {
this.wrappee = source;
}

@Override
public void writeData(String data) {
wrappee.writeData(data);
}

@Override
public String readData() {
return wrappee.readData();
}
}


public class EncryptionDecorator extends DataSourceDecorator {

public EncryptionDecorator(DataSource source) {
super(source);
}

@Override
public void writeData(String data) {
super.writeData(encode(data));
}

@Override
public String readData() {
return decode(super.readData());
}

private String encode(String data) {
byte[] result = data.getBytes();
for (int i = 0; i < result.length; i++) {
result[i] += (byte) 1;
}
return Base64.getEncoder().encodeToString(result);
}

private String decode(String data) {
byte[] result = Base64.getDecoder().decode(data);
for (int i = 0; i < result.length; i++) {
result[i] -= (byte) 1;
}
return new String(result);
}
}


public class CompressionDecorator extends DataSourceDecorator {
private int compLevel = 6;

public CompressionDecorator(DataSource source) {
super(source);
}

public int getCompressionLevel() {
return compLevel;
}

public void setCompressionLevel(int value) {
compLevel = value;
}

@Override
public void writeData(String data) {
super.writeData(compress(data));
}

@Override
public String readData() {
return decompress(super.readData());
}

private String compress(String stringData) {
byte[] data = stringData.getBytes();
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater(compLevel));
dos.write(data);
dos.close();
bout.close();
return Base64.getEncoder().encodeToString(bout.toByteArray());
} catch (IOException ex) {
return null;
}
}

private String decompress(String stringData) {
byte[] data = Base64.getDecoder().decode(stringData);
try {
InputStream in = new ByteArrayInputStream(data);
InflaterInputStream iin = new InflaterInputStream(in);
ByteArrayOutputStream bout = new ByteArrayOutputStream(512);
int b;
while ((b = iin.read()) != -1) {
bout.write(b);
}
in.close();
iin.close();
bout.close();
return new String(bout.toByteArray());
} catch (IOException ex) {
return null;
}
}
}


public class Demo {
public static void main(String[] args) {
String salaryRecords = "Name,Salary\nJohn Smith,100000\nSteven Jobs,912000";
DataSourceDecorator encoded = new CompressionDecorator(
new EncryptionDecorator(
new FileDataSource("out/OutputDemo.txt")
)
);
encoded.writeData(salaryRecords);
DataSource plain = new FileDataSource("out/OutputDemo.txt");

System.out.println("- Input ----------------");
System.out.println(salaryRecords);
System.out.println("- Encoded --------------");
System.out.println(plain.readData());
System.out.println("- Decoded --------------");
System.out.println(encoded.readData());
}
}
1
2
3
4
5
6
7
8
9
10
- Input ----------------
Name,Salary
John Smith,100000
Steven Jobs,912000
- Encoded --------------
Zkt7e1Q5eU8yUm1Qe0ZsdHJ2VXp6dDBKVnhrUHtUe0sxRUYxQkJIdjVLTVZ0dVI5Q2IwOXFISmVUMU5rcENCQmdxRlByaD4+
- Decoded --------------
Name,Salary
John Smith,100000
Steven Jobs,912000

적용

데코레이터 패턴은 이 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 때 사용하세요.

데코레이터는 비즈니스 로직을 계층으로 구성하고, 각 계층에 데코레이터를 생성하고 런타임에 이 로직의 다양한 조합들로 객체들을 구성할 수 있도록 합니다. 이러한 모든 객체가 공통 인터페이스를 따르기 때문에 클라이언트 코드는 해당 모든 객체를 같은 방식으로 다룰 수 있습니다.

이 패턴은 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 때 사용하세요.

많은 프로그래밍 언어에는 클래스의 추가 확장을 방지하는 데 사용할 수 있는 final 키워드가 있습니다. Final 클래스의 경우 기존 행동들을 재사용할 수 있는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것입니다.

장단점

장점

  • 새 자식 클래스를 만들지 않고도 객체의 행동을 확장할 수 있습니다.
  • 런타임에 객체들에서부터 책임들을 추가하거나 제거할 수 있습니다.
  • 객체를 여러 데코레이터로 래핑하여 여러 행동들을 합성할 수 있습니다.
  • 단일 책임 원칙. 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있습니다.

단점

  • 래퍼들의 스택에서 특정 래퍼를 제거하기가 어렵습니다.
  • 데코레이터의 행동이 데코레이터 스택 내의 순서에 의존하지 않는 방식으로 데코레이터를 구현하기가 어렵습니다.
  • 계층들의 초기 설정 코드가 보기 흉할 수 있습니다.

퍼사드 패턴

의도

퍼사드 패턴은 라이브러리에 대한, 프레임워크에 대한 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적 디자인 패턴입니다.

구조

스크린샷 2023-03-27 오후 10.28.21

  1. 퍼사드 패턴을 사용하면 하위 시스템 기능들의 특정 부분에 편리하게 접근할 수 있습니다. 또 퍼사드는 클라이언트의 요청을 어디로 보내야 하는지와 움직이는 모든 부품을 어떻게 작동해야 하는지를 알고 있습니다.

  2. 추가적인 퍼사드 클래스를 생성하여 하나의 퍼사드를 관련 없는 기능들로 오염시켜 복잡한 구조로 만드는 것을 방지할 수 있습니다. 추가 퍼사드들은 클라이언트들과 다른 퍼사드들 모두에 사용할 수 있습니다.

  3. 복잡한 하위 시스템은 수십 개의 다양한 객체들로 구성됩니다. 이 모든 객체가 의미 있는 작업을 수행하도록 하려면, 하위 시스템의 세부적인 구현 정보를 깊이 있게 살펴야 합니다. 예를 들어 올바른 순서로 객체들을 초기화하고 그들에게 적절한 형식의 데이터를 제공하는 등의 작업을 수행해야 합니다.

    하위 시스템 클래스들은 퍼사드의 존재를 인식하지 못합니다. 이들은 시스템 내에서 작동하며, 매개체 없이 직접 서로와 작업합니다.

  4. 클라이언트는 하위 시스템 객체들을 직접 호출하는 대신 퍼사드를 사용합니다.

예시

스크린샷 2023-03-27 오후 10.29.14

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
public class VideoFile {
private String name;
private String codecType;

public VideoFile(String name) {
this.name = name;
this.codecType = name.substring(name.indexOf(".") + 1);
}

public String getCodecType() {
return codecType;
}

public String getName() {
return name;
}
}


public interface Codec {
}


public class MPEG4CompressionCodec implements Codec {
public String type = "mp4";

}

public class OggCompressionCodec implements Codec {
public String type = "ogg";
}


public class CodecFactory {
public static Codec extract(VideoFile file) {
String type = file.getCodecType();
if (type.equals("mp4")) {
System.out.println("CodecFactory: extracting mpeg audio...");
return new MPEG4CompressionCodec();
}
else {
System.out.println("CodecFactory: extracting ogg audio...");
return new OggCompressionCodec();
}
}
}


public class BitrateReader {
public static VideoFile read(VideoFile file, Codec codec) {
System.out.println("BitrateReader: reading file...");
return file;
}

public static VideoFile convert(VideoFile buffer, Codec codec) {
System.out.println("BitrateReader: writing file...");
return buffer;
}
}

public class AudioMixer {
public File fix(VideoFile result){
System.out.println("AudioMixer: fixing audio...");
return new File("tmp");
}
}

public class VideoConversionFacade {
public File convertVideo(String fileName, String format) {
System.out.println("VideoConversionFacade: conversion started.");
VideoFile file = new VideoFile(fileName);
Codec sourceCodec = CodecFactory.extract(file);
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new MPEG4CompressionCodec();
} else {
destinationCodec = new OggCompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
File result = (new AudioMixer()).fix(intermediateResult);
System.out.println("VideoConversionFacade: conversion completed.");
return result;
}
}


public class Demo {
public static void main(String[] args) {
VideoConversionFacade converter = new VideoConversionFacade();
File mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4");
// ...
}
}
1
2
3
4
5
6
VideoConversionFacade: conversion started.
CodecFactory: extracting ogg audio...
BitrateReader: reading file...
BitrateReader: writing file...
AudioMixer: fixing audio...
VideoConversionFacade: conversion completed.

적용

퍼사드 패턴은 당신이 복잡한 하위 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 때 사용하세요.

하위 시스템은 시간이 지날수록 더 복잡해지곤 합니다. 디자인 패턴들을 적용하더라도 보통은 생성되는 클래스들이 점점 더 많아지게 됩니다. 하위 시스템은 더 유연해지고 더 많은 다양한 상황에서 재사용할 수 있도록 변경될 수도 있지만, 해당 시스템이 클라이언트에게 요구하는 설정 및 상용구 코드의 양은 점점 더 많아질 것입니다. 이 문제를 해결하기 위해 퍼사드는 대부분의 클라이언트 요건에 부합하면서 하위 시스템에서 가장 많이 사용되는 기능들로 향하는 지름길을 제공합니다.

퍼사드 패턴은 하위 시스템을 계층들로 구성하려는 경우 사용하세요.

하위시스템의 각 계층에 대한 진입점을 정의하기 위해 퍼사드 패턴들을 생성하세요. 당신은 여러 하위시스템이 퍼사드 패턴들을 통해서만 통신하도록 함으로써 해당 하위시스템 간의 결합도를 줄일 수 있습니다.

예를 들어 당신의 비디오 변환 프레임워크는 비디오 또는 오디오 관련의 두 가지 계층으로 나뉠 수 있습니다. 각 계층에 대해 퍼사드를 만든 다음 각 계층의 클래스들이 해당 퍼사드들을 통해 서로 통신하도록 할 수 있습니다. 이러한 접근 방식은 중재자 패턴과 매우 유사합니다.

장단점

장점

  • 복잡한 하위 시스템에서 코드를 별도로 분리할 수 있습니다.

단점

  • 퍼사드는 앱의 모든 클래스에 결합된 전지전능한 객체가 될 수 있습니다.

플라이웨이트

의도

플라이웨이트는 각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 구조 디자인 패턴입니다.

구조

스크린샷 2023-03-27 오후 10.32.14

  1. 플라이웨이트 패턴은 단지 최적화에 불과합니다. 이 패턴을 적용하기 전에 프로그램이 동시에 메모리에 유사한 객체들을 대량으로 보유하는 것과 관련된 RAM 소비 문제가 있는지 확인하시고 이 문제가 다른 의미 있는 방법으로 해결될 수 없는지도 확인하세요.
  2. 플라이웨이트 클래스에는 여러 객체들 간에 공유할 수 있는 원래 객체의 상태의 부분이 포함됩니다. 같은 플라이웨이트 객체가 다양한 콘텍스트에서 사용될 수 있습니다. 플라이웨이트 내부에 저장된 상태를 고유한(intrinsic) 상태라고 하며, 플라이웨이트의 메서드에 전달된 상태를 공유한(extrinsic) 상태라고 합니다.
  3. 콘텍스트 클래스는 공유한 상태를 포함하며, 이 상태는 모든 원본 객체들에서 고유합니다. 콘텍스트가 플라이웨이트 객체 중 하나와 쌍을 이루면 원래 객체의 전체 상태를 나타냅니다.
  4. 일반적으로 원래 객체의 행동은 플라이웨이트 클래스에 남아 있습니다. 이 경우 플라이웨이트의 메서드의 호출자는 공유한 상태의 적절한 부분들을 메서드의 매개변수들에 전달해야 합니다. 반면에, 행동은 콘텍스트 클래스로 이동할 수 있으며, 이 클래스는 연결된 플라이웨이트를 단순히 데이터 객체로 사용할 것입니다.
  5. 클라이언트는 플라이웨이트들의 공유된 상태를 저장하거나 계산합니다. 클라이언트의 관점에서 플라이웨이트는 일부 콘텍스트 데이터를 그의 메서드들의 매개변수들에 전달하여 런타임에 설정될 수 있는 템플릿 객체입니다.
  6. 플라이웨이트 팩토리는 기존 플라이웨이트들의 풀을 관리합니다. 이 팩토리로 인해 클라이언트들은 플라이웨이트들을 직접 만들지 않는 대신 원하는 플라이웨이트의 고유한 상태의 일부를 전달하여 공장을 호출합니다. 팩토리는 이전에 생성된 플라이웨이트들을 살펴보고 검색 기준과 일치하는 기존 플라이웨이트를 반환하거나 기준에 맞는 플라이웨이트가 발견되지 않으면 새로 생성합니다.

예시

스크린샷 2023-03-27 오후 10.33.01

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
public class Tree {
private int x;
private int y;
private TreeType type;

public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}

public void draw(Graphics g) {
type.draw(g, x, y);
}
}


public class TreeType {
private String name;
private Color color;
private String otherTreeData;

public TreeType(String name, Color color, String otherTreeData) {
this.name = name;
this.color = color;
this.otherTreeData = otherTreeData;
}

public void draw(Graphics g, int x, int y) {
g.setColor(Color.BLACK);
g.fillRect(x - 1, y, 3, 5);
g.setColor(color);
g.fillOval(x - 5, y - 10, 10, 10);
}
}


public class TreeFactory {
static Map<String, TreeType> treeTypes = new HashMap<>();

public static TreeType getTreeType(String name, Color color, String otherTreeData) {
TreeType result = treeTypes.get(name);
if (result == null) {
result = new TreeType(name, color, otherTreeData);
treeTypes.put(name, result);
}
return result;
}
}


public class Forest extends JFrame {
private List<Tree> trees = new ArrayList<>();

public void plantTree(int x, int y, String name, Color color, String otherTreeData) {
TreeType type = TreeFactory.getTreeType(name, color, otherTreeData);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}

@Override
public void paint(Graphics graphics) {
for (Tree tree : trees) {
tree.draw(graphics);
}
}
}


public class Demo {
static int CANVAS_SIZE = 500;
static int TREES_TO_DRAW = 1000000;
static int TREE_TYPES = 2;

public static void main(String[] args) {
Forest forest = new Forest();
for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Summer Oak", Color.GREEN, "Oak texture stub");
forest.plantTree(random(0, CANVAS_SIZE), random(0, CANVAS_SIZE),
"Autumn Oak", Color.ORANGE, "Autumn Oak texture stub");
}
forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
forest.setVisible(true);

System.out.println(TREES_TO_DRAW + " trees drawn");
System.out.println("---------------------");
System.out.println("Memory usage:");
System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW);
System.out.println("+ TreeTypes size (~30 bytes) * " + TREE_TYPES + "");
System.out.println("---------------------");
System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 30) / 1024 / 1024) +
"MB (instead of " + ((TREES_TO_DRAW * 38) / 1024 / 1024) + "MB)");
}

private static int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
}
1
2
3
4
5
6
7
1000000 trees drawn
---------------------
Memory usage:
Tree size (8 bytes) * 1000000
+ TreeTypes size (~30 bytes) * 2
---------------------
Total: 7MB (instead of 36MB)

적용

플라이웨이트 패턴은 당신의 프로그램이 많은 수의 객체들을 지원해야 해서 사용할 수 있는 RAM을 거의 다 사용했을 때만 사용하세요.

이 패턴 적용의 혜택은 패턴을 사용하는 방법과 위치에 따라 크게 달라지며, 다음과 같은 경우에 가장 유용합니다.

  • 앱이 수많은 유사 객체들을 생성해야 할 때
  • 이것이 대상 장치에서 사용할 수 있는 모든 RAM을 소모할 때
  • 이 객체들에 여러 중복 상태들이 포함되어 있으며, 이 상태들이 추출된 후 객체 간에 공유될 수 있을 때

장단점

장점

  • 당신의 프로그램에 유사한 객체들이 많다고 가정하면 많은 RAM을 절약할 수 있습니다.

단점

  • 누군가가 플라이웨이트 메서드를 호출할 때마다 콘텍스트 데이터의 일부를 다시 계산해야 한다면 당신은 CPU 주기 대신 RAM을 절약하고 있는 것일지도 모릅니다.
  • 코드가 복잡해지므로 새로운 팀원들은 왜 개체(entity)의 상태가 그런 식으로 분리되었는지 항상 궁금해할 것입니다.

프록시 패턴

의도

프록시는 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴입니다. 프록시는 원래 객체에 대한 접근을 제어하므로, 당신의 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 합니다.

즉, 기존 로직은 그대로 두고, 추가적인 캐싱, valid등 전후 처리가 필요할때, Proxy를 사용하면 좋다

구조

스크린샷 2023-03-27 오후 10.35.44

  1. 서비스 인터페이스는 서비스의 인터페이스를 선언합니다. 프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 합니다.

  2. 서비스는 어떤 유용한 비즈니스 로직을 제공하는 클래스입니다.

  3. 프록시 클래스에는 서비스 객체를 가리키는 참조 필드가 있습니다. 프록시가 요청의 처리(예: 초기화 지연, 로깅, 액세스 제어, 캐싱 등)를 완료하면, 그 후 처리된 요청을 서비스 객체에 전달합니다.

    일반적으로 프록시들은 서비스 객체들의 전체 수명 주기를 관리합니다.

  4. 클라이언트는 같은 인터페이스를 통해 서비스들 및 프록시들과 함께 작동해야 합니다. 그러면 서비스 객체를 기대하는 모든 코드에 프록시를 전달할 수 있기 때문입니다.

예시

이 라이브러리는 비디오 다운로드 클래스를 제공하나 매우 비효율적입니다. 왜냐하면 클라이언트 앱이 같은 비디오를 여러 번 요청하면 라이브러리는 처음 다운로드한 파일을 캐싱하고 재사용하는 대신 계속해서 같은 비디오를 다운로드하기 때문입니다.

프록시 클래스는 원래 다운로더와 같은 인터페이스를 구현하고 이 다운로더에 모든 작업을 위임하나, 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적한 후 캐시 된 결과를 반환합니다.

스크린샷 2023-03-27 오후 10.36.05

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
public interface ThirdPartyYouTubeLib {
HashMap<String, Video> popularVideos();

Video getVideo(String videoId);
}


public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {

@Override
public HashMap<String, Video> popularVideos() {
connectToServer("http://www.youtube.com");
return getRandomVideos();
}

@Override
public Video getVideo(String videoId) {
connectToServer("http://www.youtube.com/" + videoId);
return getSomeVideo(videoId);
}

// -----------------------------------------------------------------------
// Fake methods to simulate network activity. They as slow as a real life.

private int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}

private void experienceNetworkLatency() {
int randomLatency = random(5, 10);
for (int i = 0; i < randomLatency; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

private void connectToServer(String server) {
System.out.print("Connecting to " + server + "... ");
experienceNetworkLatency();
System.out.print("Connected!" + "\n");
}

private HashMap<String, Video> getRandomVideos() {
System.out.print("Downloading populars... ");

experienceNetworkLatency();
HashMap<String, Video> hmap = new HashMap<String, Video>();
hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));

System.out.print("Done!" + "\n");
return hmap;
}

private Video getSomeVideo(String videoId) {
System.out.print("Downloading video... ");

experienceNetworkLatency();
Video video = new Video(videoId, "Some video title");

System.out.print("Done!" + "\n");
return video;
}

}



public class Video {
public String id;
public String title;
public String data;

Video(String id, String title) {
this.id = id;
this.title = title;
this.data = "Random video.";
}
}

public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
private ThirdPartyYouTubeLib youtubeService;
private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
private HashMap<String, Video> cacheAll = new HashMap<String, Video>();

public YouTubeCacheProxy() {
this.youtubeService = new ThirdPartyYouTubeClass();
}

@Override
public HashMap<String, Video> popularVideos() {
if (cachePopular.isEmpty()) {
cachePopular = youtubeService.popularVideos();
} else {
System.out.println("Retrieved list from cache.");
}
return cachePopular;
}

@Override
public Video getVideo(String videoId) {
Video video = cacheAll.get(videoId);
if (video == null) {
video = youtubeService.getVideo(videoId);
cacheAll.put(videoId, video);
} else {
System.out.println("Retrieved video '" + videoId + "' from cache.");
}
return video;
}

public void reset() {
cachePopular.clear();
cacheAll.clear();
}
}


public class YouTubeDownloader {
private ThirdPartyYouTubeLib api;

public YouTubeDownloader(ThirdPartyYouTubeLib api) {
this.api = api;
}

public void renderVideoPage(String videoId) {
Video video = api.getVideo(videoId);
System.out.println("\n-------------------------------");
System.out.println("Video page (imagine fancy HTML)");
System.out.println("ID: " + video.id);
System.out.println("Title: " + video.title);
System.out.println("Video: " + video.data);
System.out.println("-------------------------------\n");
}

public void renderPopularVideos() {
HashMap<String, Video> list = api.popularVideos();
System.out.println("\n-------------------------------");
System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
for (Video video : list.values()) {
System.out.println("ID: " + video.id + " / Title: " + video.title);
}
System.out.println("-------------------------------\n");
}
}


public class Demo {

public static void main(String[] args) {
YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());

long naive = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.print("Time saved by caching proxy: " + (naive - smart) + "ms");

}

private static long test(YouTubeDownloader downloader) {
long startTime = System.currentTimeMillis();

// User behavior in our app:
downloader.renderPopularVideos();
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderPopularVideos();
downloader.renderVideoPage("dancesvideoo");
// Users might visit the same page quite often.
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderVideoPage("someothervid");

long estimatedTime = System.currentTimeMillis() - startTime;
System.out.print("Time elapsed: " + estimatedTime + "ms\n");
return estimatedTime;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Connecting to http://www.youtube.com... Connected!
Downloading populars... Done!

-------------------------------
Most popular videos on YouTube (imagine fancy HTML)
ID: sadgahasgdas / Title: Catzzzz.avi
ID: asdfas3ffasd / Title: Dancing video.mpq
ID: 3sdfgsd1j333 / Title: Programing lesson#1.avi
ID: mkafksangasj / Title: Dog play with ball.mp4
ID: dlsdk5jfslaf / Title: Barcelona vs RealM.mov
-------------------------------

Connecting to http://www.youtube.com/catzzzzzzzzz... Connected!
Downloading video... Done!

-------------------------------
Video page (imagine fancy HTML)
ID: catzzzzzzzzz
Title: Some video title
Video: Random video.
-------------------------------

적용

지연된 초기화(가상 프록시). 이것은 어쩌다 필요한 무거운 서비스 객체가 항상 가동되어 있어 시스템 자원들을 낭비하는 경우입니다.

앱이 시작될 때 객체를 생성하는 대신, 객체 초기화를 실제로 초기화가 필요한 시점까지 지연할 수 있습니다.

접근 제어 (보호 프록시). 당신이 특정 클라이언트들만 서비스 객체를 사용할 수 있도록 하려는 경우에 사용할 수 있습니다. 예를 들어 당신의 객체들이 운영 체제의 중요한 부분이고 클라이언트들이 다양한 실행된 응용 프로그램(악의적인 응용 프로그램 포함)인 경우입니다.

이 프록시는 클라이언트의 자격 증명이 어떤 정해진 기준과 일치하는 경우에만 서비스 객체에 요청을 전달할 수 있습니다.

원격 서비스의 로컬 실행 (원격 프록시). 서비스 객체가 원격 서버에 있는 경우입니다.

이 경우 프록시는 네트워크를 통해 클라이언트 요청을 전달하여 네트워크와의 작업의 모든 복잡한 세부 사항을 처리합니다.

요청들의 로깅(로깅 프록시). 서비스 객체에 대한 요청들의 기록을 유지하려는 경우입니다.

프록시는 각 요청을 서비스에 전달하기 전에 로깅(기록)할 수 있습니다.

요청 결과들의 캐싱(캐싱 프록시). 이것은 클라이언트 요청들의 결과들을 캐시하고 이 캐시들의 수명 주기를 관리해야 할 때, 특히 결과들이 상당히 큰 경우에 사용됩니다.

프록시는 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현할 수 있습니다. 프록시는 요청들의 매개변수들을 캐시 키들로 사용할 수 있습니다.

스마트 참조. 이것은 사용하는 클라이언트들이 없어 거대한 객체를 해제할 수 있어야 할 때 사용됩니다.

프록시는 서비스 객체 또는 그 결과에 대한 참조를 얻은 클라이언트들을 추적할 수 있습니다. 때때로 프록시는 클라이언트들을 점검하여 클라이언트들이 여전히 활성 상태인지를 확인할 수 있습니다. 클라이언트 리스트가 비어 있으면 프록시는 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원을 확보할 수 있습니다.

또 프록시는 클라이언트가 서비스 객체를 수정했는지도 추적할 수 있으며, 변경되지 않은 객체는 다른 클라이언트들이 재사용할 수 있습니다.

장단점

장점

  • 클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어할 수 있습니다.
  • 클라이언트들이 신경 쓰지 않을 때 서비스 객체의 수명 주기를 관리할 수 있습니다.
  • 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동합니다.
  • 개방/폐쇄 원칙. 서비스나 클라이언트들을 변경하지 않고도 새 프록시들을 도입할 수 있습니다.

단점

  • 새로운 클래스들을 많이 도입해야 하므로 코드가 복잡해질 수 있습니다.
  • 서비스의 응답이 늦어질 수 있습니다.
CATALOG
  1. 1. 구조 패턴
  2. 2. 어댑터 패턴
    1. 2.1. 의도
    2. 2.2. 객체 어댑터
    3. 2.3. 예시
    4. 2.4. 적용
      1. 2.4.1. 어댑터 클래스는 기존 클래스를 사용하고 싶지만 그 인터페이스가 나머지 코드와 호환되지 않을 때 사용하세요.
      2. 2.4.2. 이 패턴은 부모 클래스에 추가할 수 없는 어떤 공통 기능들이 없는 여러 기존 자식 클래스들을 재사용하려는 경우에 사용하세요.
    5. 2.5. 장단점
      1. 2.5.1. 장점
      2. 2.5.2. 단점
  3. 3. 브리지 패턴
    1. 3.1. 의도
    2. 3.2. 구조
    3. 3.3. 예시
    4. 3.4. 적용
    5. 3.5. 장단점
      1. 3.5.1. 장점
      2. 3.5.2. 단점
  4. 4. 복합체 패턴
    1. 4.1. 의도
    2. 4.2. 구조
    3. 4.3. 예시
    4. 4.4. 적용
    5. 4.5. 장단점
      1. 4.5.1. 장점
      2. 4.5.2. 단점
  5. 5. 데코레이터 패턴
    1. 5.1. 의도
    2. 5.2. 구조
    3. 5.3. 예시
    4. 5.4. 적용
    5. 5.5. 장단점
      1. 5.5.1. 장점
      2. 5.5.2. 단점
  6. 6. 퍼사드 패턴
    1. 6.1. 의도
    2. 6.2. 구조
    3. 6.3. 예시
    4. 6.4. 적용
    5. 6.5. 장단점
      1. 6.5.1. 장점
      2. 6.5.2. 단점
  7. 7. 플라이웨이트
    1. 7.1. 의도
    2. 7.2. 구조
    3. 7.3. 예시
    4. 7.4. 적용
    5. 7.5. 장단점
      1. 7.5.1. 장점
      2. 7.5.2. 단점
  8. 8. 프록시 패턴
    1. 8.1. 의도
    2. 8.2. 구조
    3. 8.3. 예시
    4. 8.4. 적용
    5. 8.5. 장단점
      1. 8.5.1. 장점
      2. 8.5.2. 단점