🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
CS
GOF 디자인패턴-JAVA
02-구조 패턴

1. 어댑터(Adapter) 패턴

1.1 패턴 소개

gof_2_1

기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴

  • 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.
  • 즉, 서로 다른 인터페이스를 가진 두 객체를 연결해주는 디자인 패턴이다.

gof_2_2

e.g. 110v용 콘센트220v용 콘센트에 꽂는다던가, 반대로 220v용 콘센트110v용 콘센트에 꽂을 때,

  • 이 중간에 쓰이는 어댑터(돼지코)가 바로 일상에서 볼 수 있는 어댑터 패턴이다.

1.2 예시 - 기존

1
// UserDetails.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;
3
4
public interface UserDetails {
5
6
String getUsername();
7
8
String getPassword();
9
10
}
1
// UserDetailsService.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;
3
4
public interface UserDetailsService {
5
6
UserDetails loadUser(String username);
7
8
}
1
// LoginHandler.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;
3
4
public class LoginHandler {
5
6
UserDetailsService userDetailsService;
7
8
public LoginHandler(UserDetailsService userDetailsService) {
9
this.userDetailsService = userDetailsService;
10
}
11
12
public String login(String username, String password) {
13
UserDetails userDetails = userDetailsService.loadUser(username);
14
if (userDetails.getPassword().equals(password)) {
15
return userDetails.getUsername();
16
} else {
17
throw new IllegalArgumentException();
18
}
19
}
20
}
1
// Account.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;
3
4
public class Account {
5
6
private String name;
7
8
private String password;
9
10
private String email;
11
12
public String getName() {
13
return name;
14
}
15
16
public void setName(String name) {
17
this.name = name;
18
}
19
20
public String getPassword() {
21
return password;
22
}
23
24
public void setPassword(String password) {
25
this.password = password;
26
}
27
28
public String getEmail() {
29
return email;
30
}
31
32
public void setEmail(String email) {
33
this.email = email;
34
}
35
36
}
1
// AccountService.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;
3
4
public class AccountService {
5
6
public Account findAccountByUsername(String username) {
7
Account account = new Account();
8
account.setName(username);
9
account.setPassword(username);
10
account.setEmail(username);
11
return account;
12
}
13
14
public void createNewAccount(Account account) {
15
16
}
17
18
public void updateAccount(Account account) {
19
20
}
21
22
}

1.3 예시 - 변경

1
// AccountUserDetails.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;
3
4
import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetails;
5
6
public class AccountUserDetails implements UserDetails {
7
8
private Account account;
9
10
public AccountUserDetails(Account account) {
11
this.account = account;
12
}
13
14
@Override
15
public String getUsername() {
16
return account.getName();
17
}
18
19
@Override
20
public String getPassword() {
21
return account.getPassword();
22
}
23
}
1
// AccountUserDetailsService.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;
3
4
import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetails;
5
import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetailsService;
6
7
public class AccountUserDetailsService implements UserDetailsService {
8
9
private AccountService accountService;
10
11
public AccountUserDetailsService(AccountService accountService) {
12
this.accountService = accountService;
13
}
14
15
@Override
16
public UserDetails loadUser(String username) {
17
return new AccountUserDetails(accountService.findAccountByUsername(username));
18
}
19
}
1
// App.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;
3
4
import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.LoginHandler;
5
import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetailsService;
6
7
public class App {
8
9
public static void main(String[] args) {
10
AccountService accountService = new AccountService();
11
UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
12
LoginHandler loginHandler = new LoginHandler(userDetailsService);
13
String login = loginHandler.login("keesun", "keesun");
14
System.out.println(login);
15
}
16
}

1.4 장단점

  • 장점
    • 기존 코드를 변경하지 않고, 원하는 인터페이스 구현체를 만들어 재사용할 수 있다.
    • 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다.
  • 단점
    • 새 클래스가 생겨 복잡도가 증가할 수 있다.
    • 경우에 따라서는 기존 코드가 해당 인터페이스를 구현하도록 수정하는 것이 좋은 선택이 될 수도 있다

1.5 실무에서는 어떻게 쓰이나?

자바

  • java.util.Arrays#asList(T…)
  • java.util.Collections#list(Enumeration), java.util.Collections#enumeration()
  • java.io.InputStreamReader(InputStream)
  • java.io.OutputStreamWriter(OutputStream)
1
// AdapterInJava.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._03_java;
3
4
import java.io.*;
5
import java.util.*;
6
7
public class AdapterInJava {
8
9
public static void main(String[] args) {
10
// collections
11
List<String> strings = Arrays.asList("a", "b", "c");
12
Enumeration<String> enumeration = Collections.enumeration(strings);
13
ArrayList<String> list = Collections.list(enumeration);
14
15
// io
16
try (
17
InputStream is = new FileInputStream("input.txt");
18
InputStreamReader isr = new InputStreamReader(is);
19
BufferedReader reader = new BufferedReader(isr)
20
) {
21
while (reader.ready()) {
22
System.out.println(reader.readLine());
23
}
24
} catch (IOException e) {
25
throw new RuntimeException(e);
26
}
27
}
28
}

스프링

  • HandlerAdpter: 우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행할 수 있는 형태로 변환해주는 어댑터용 인터페이스
1
// AdapterInSpring.java
2
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._03_java;
3
4
import org.springframework.web.servlet.DispatcherServlet;
5
import org.springframework.web.servlet.HandlerAdapter;
6
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
7
8
public class AdapterInSpring {
9
10
public static void main(String[] args) {
11
DispatcherServlet dispatcherServlet = new DispatcherServlet();
12
HandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
13
}
14
}

2. 브릿지(Bridge) 패턴

2.1 패턴 소개

gof_2_3

추상적인 것구체적인 것을 분리하여 연결하는 패턴

  • 하나의 계층 구조일 때 보다 각기 나누었을 때 독립적인 계층 구조로 발전 시킬 수 있다.

  • 어댑터 패턴서로 상이한 두 인터페이스를 연결하는 것이라면,

    • 브릿지 패턴추상적인 것과 구체적인 것 사이를 연결하는 브릿지를 만들기 위해 상속이 아닌, 컴포지트를 사용
    • 그래서 나눠서 연결한다는 의미로 bridge(다리)를 사용한다.
  • 위 그림의 중간 Abstration은 추상적인 부분, 위 그림의 가장 오른쪽 Implementation은 구체적인 부분이다.

    • 여기서 중요한 건 추상적인 부분과 구체적인 부분을 연결해주는 다리 역할을 하는 화살표이다.
    • 어떤 동작들만 있는 것, 상태만 모아놓은 어떤 프론트엔드, 백엔드만 모아놓은 것과 같이
    • 서로 성격이 상이한 것들을 분리해서, 그것들을 대칭 구조가 아닌 서로 분리하고, 그 둘 사이를 연결하는 다리를 놓아주는 것

gof_2_4

e.g. 게임 롤 캐릭터인 아리와 아칼리가 있고, 그들의 스킨인 수영복과 KDA 스킨이 있다.

  • 챔피언은 게임 내에서 플레이어가 조종하는 주요 캐릭터이고,
    • 스킨은 챔피언의 외모를 변경하지만, 챔피언의 기본 능력이나 게임 플레이에는 영향을 주지 않는다.
  • 브릿지 패턴을 이용하여 이를 모델링한다면, “챔피언”을 추상화로, “스킨”을 구현으로 볼 수 있다.
  • 이렇게 하면,챔피언(아리, 아칼리)스킨(수영복, KDA)을 독립적으로 확장할 수 있다.
  1. 추상화(Abstraction) 층: 이 층에서는 챔피언의 추상적인 개념을 정의.
    • e.g. 챔피언 클래스는 모든 챔피언이 공통으로 가지고 있는 특성(예: 이름, 기본 공격)을 정의할 수 있다.
    • 그리고 이 추상화 층에서는 스킨을 적용하는 메소드를 포함할 수 있다.
  2. 구현(Implementation) 층: 이 층에서는 스킨의 구체적인 구현을 정의
    • 수영복 스킨, KDA 스킨 등 각각의 스킨은 이 구현 층에서 정의된다.
    • 각 스킨은 챔피언의 외모를 변경하는 방법을 구체적으로 구현한다.

이러한 구조를 통해, 새로운 챔피언이나 스킨을 추가할 때 기존 코드를 변경하지 않고도 확장이 가능하다.

  • e.g. 새로운 챔피언을 추가하려면, 추상화 층에 새로운 챔피언 클래스를 추가하기만 하면 되고,
  • e.g. 새로운 스킨을 추가하려면, 구현 층에 새로운 스킨 클래스를 추가하면 된다.

브릿지 패턴을 사용함으로써, 챔피언과 스킨의 결합도를 낮추고, 각각을 독립적으로 확장할 수 있는 유연성을 확보할 수 있다


2.2 예시 - 기존

1
// Skin.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
public interface Skin {
5
String getName();
6
}
1
// Champion.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;
3
4
import me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after.Skin;
5
6
public interface Champion extends Skin {
7
void move();
8
9
void skillQ();
10
11
void skillW();
12
13
void skillE();
14
15
void skillR();
16
}
1
// KDA아리.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;
3
4
public class KDA아리 implements Champion {
5
6
@Override
7
public void move() {
8
System.out.println("KDA 아리 move");
9
}
10
11
@Override
12
public void skillQ() {
13
System.out.println("KDA 아리 Q");
14
}
15
16
@Override
17
public void skillW() {
18
System.out.println("KDA 아리 W");
19
}
20
21
@Override
22
public void skillE() {
23
System.out.println("KDA 아리 E");
24
}
25
26
@Override
27
public void skillR() {
28
System.out.println("KDA 아리 R");
29
}
30
31
@Override
32
public String getName() {
33
return null;
34
}
35
}
1
// PoolParty아리.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;
3
4
public class PoolParty아리 implements Champion {
5
6
@Override
7
public void move() {
8
System.out.println("PoolParty move");
9
}
10
11
@Override
12
public void skillQ() {
13
System.out.println("PoolParty Q");
14
}
15
16
@Override
17
public void skillW() {
18
System.out.println("PoolParty W");
19
}
20
21
@Override
22
public void skillE() {
23
System.out.println("PoolParty E");
24
}
25
26
@Override
27
public void skillR() {
28
System.out.println("PoolParty R");
29
}
30
31
@Override
32
public String getName() {
33
return null;
34
}
35
}
1
// App.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;
3
4
public class App {
5
6
public static void main(String[] args) {
7
Champion kda아리 = new KDA아리();
8
kda아리.skillQ();
9
kda아리.skillR();
10
}
11
}

2.3 예시 - 변경

1
// DefaultChampion.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
import me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before.Champion;
5
6
public class DefaultChampion implements Champion {
7
8
private Skin skin;
9
10
private String name;
11
12
public DefaultChampion(Skin skin, String name) {
13
this.skin = skin;
14
this.name = name;
15
}
16
17
@Override
18
public void move() {
19
System.out.printf("%s %s move\n", skin.getName(), this.name);
20
}
21
22
@Override
23
public void skillQ() {
24
System.out.printf("%s %s Q\n", skin.getName(), this.name);
25
}
26
27
@Override
28
public void skillW() {
29
System.out.printf("%s %s W\n", skin.getName(), this.name);
30
}
31
32
@Override
33
public void skillE() {
34
System.out.printf("%s %s E\n", skin.getName(), this.name);
35
}
36
37
@Override
38
public void skillR() {
39
System.out.printf("%s %s R\n", skin.getName(), this.name);
40
}
41
42
@Override
43
public String getName() {
44
return null;
45
}
46
}
1
// KDA.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
public class KDA implements Skin {
5
6
@Override
7
public String getName() {
8
return "KDA";
9
}
10
}
1
// PoolParty.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
public class PoolParty implements Skin {
5
6
@Override
7
public String getName() {
8
return "PoolParty";
9
}
10
}
1
// 아리.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
public class 아리 extends DefaultChampion {
5
6
public 아리(Skin skin) {
7
super(skin, "아리");
8
}
9
}
1
// 아칼리.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
public class 아칼리 extends DefaultChampion {
5
6
public 아칼리(Skin skin) {
7
super(skin, "아칼리");
8
}
9
}
1
// App.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;
3
4
import me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before.Champion;
5
6
public abstract class App implements Champion {
7
8
public static void main(String[] args) {
9
Champion kda아리 = new 아리(new KDA());
10
kda아리.skillQ();
11
kda아리.skillW();
12
13
Champion poolParty아리 = new 아리(new PoolParty());
14
poolParty아리.skillR();
15
poolParty아리.skillW();
16
}
17
}

2.4 장단점

  • 장점
    • 추상적인 코드구체적인 코드 변경 없이도 독립적으로 확장할 수 있다.
    • 추상적인 코드구체적인 코드를 분리하여 수 있다.
  • 단점
    • 계층 구조가 늘어나 복잡도가 증가할 수 있다

2.5 실무에서 어떻게 쓰이나?

자바

  • JDBC API, DriverManger와 Driver
1
// JdbcExample.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;
3
4
import java.sql.*;
5
6
public class JdbcExample {
7
8
public static void main(String[] args) throws ClassNotFoundException {
9
Class.forName("org.h2.Driver");
10
11
try (
12
Connection conn = DriverManager.getConnection(
13
"jdbc:h2:mem:~/test",
14
"sa",
15
""
16
)
17
) {
18
String sql =
19
"CREATE TABLE ACCOUNT " +
20
"(id INTEGER not NULL, " +
21
" email VARCHAR(255), " +
22
" password VARCHAR(255), " +
23
" PRIMARY KEY ( id ))";
24
25
Statement statement = conn.createStatement();
26
statement.execute(sql);
27
// PreparedStatement statement1 = conn.prepareStatement(sql);
28
// ResultSet resultSet = statement.executeQuery(sql);
29
} catch (SQLException e) {
30
throw new RuntimeException(e);
31
}
32
}
33
}

Java의 SLF4J, 로깅 퍼사드와 로거

1
// Slf4jExample.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;
3
4
import org.slf4j.Logger;
5
import org.slf4j.LoggerFactory;
6
7
public class Slf4jExample {
8
9
private static Logger logger = LoggerFactory.getLogger(Slf4jExample.class);
10
11
public static void main(String[] args) {
12
logger.info("hello logger");
13
}
14
}

스프링의 Portable Service Abstraction

1
// BridgeInSpring.java
2
package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;
3
4
import org.springframework.jdbc.support.JdbcTransactionManager;
5
import org.springframework.mail.MailSender;
6
import org.springframework.mail.javamail.JavaMailSenderImpl;
7
import org.springframework.transaction.PlatformTransactionManager;
8
9
public class BridgeInSpring {
10
11
public static void main(String[] args) {
12
MailSender mailSender = new JavaMailSenderImpl();
13
14
PlatformTransactionManager platformTransactionManager = new JdbcTransactionManager();
15
}
16
}

3. 컴포짓(Composite) 패턴

3.1 패턴 소개

gof_2_5

그룹 전체개별 객체를 동일하게 처리할 수 있는 패턴.

  • 클라이언트 입장에서는 ‘전체’나 ‘부분’이나 모두 동일한 컴포넌트로 인식하는 계층 구조를 만든다. (Part-Whole Hierarchy)
  • 이는 트리 구조로 구성해야만 컴포짓 패턴을 구현할 수 있다.

gof_2_6

e.g. 게임에서 도란검(450원), 체력물약(50원)이 있다. 그리고 이 아이템들을 가방에 넣어놨다.

  • 게임에서 아이템은 단일 아이템일 수도 있고, 여러 아이템이 조합된 세트 아이템일 수도 있습니다.
  • 클라이언트 입장에서는 아이템의 가격을 개별로 출력하고 싶을 떄도 있고,
    • 반대로 가방 전체에 들어있는 아이템 가격을 알고 싶을 떄도 있을 것이다.
    • 아이템 가격은 그냥 출력하면 되는데, 가방의 아이템은 모든 아이템들을 순회하면서 출력해야 한다.
    • 이러면 가방의 모든 값을 구하는 로직이 클라이언트 측에 남는데, 이것이 객체지향적으로 올바른가에 대한 고민이 생긴다.
  • 이는 컴포짓 패턴을 적용하면 이 문제를 쉽게 해결할 수 있다.
    • 값을 구할 수 있는 모든 컴포넌트들의 공통 인터페이스를 정의한다.
      • 그래서 클라이언트는 컴포넌트라는 인터페이스 타입만 본다.
    • 우리가 만들 아이템, 백, 캐릭터 등은 leaf 또는 composite 객체가 된다.
      • leaf 객체는 가장 기본적인 단위가 되고,
      • 여러 기본적인 단위 타입들을 그룹으로 가져가는 composite 타입의 객체가 있다.
    • composite 객체는 여러 개의 컴포넌트들을 배열 또는 리스트로 가지고 있다.
      • 하지만 이떄도 타입은 절대 leaf 타입이 아니다. 컴포넌트 타입이다.
      • 그래서 컴포짓을 써도 컴포넌트 타입으로 leaf 또는 bag 또는 composite를 참조할 수 있는 것이다.
  1. 컴포넌트(Component) 인터페이스:
    • 인터페이스는 개별 아이템(도란검, 체력물약)과 세트 아이템 모두가 구현해야 하는 공통의 메서드를 정의
    • e.g. getPrice() 메서드는 아이템의 가격을 반환.
  2. 리프(Leaf) 클래스:
    • 이 클래스는 개별 아이템을 나타낸다.
    • 도란검과 체력물약은 각각 리프 클래스의 인스턴스로 표현될 수 있으며, getPrice() 메서드를 통해 자신의 가격을 반환한다.
  3. 컴포지트(Composite) 클래스:
    • 이 클래스는 여러 개의 아이템(리프 또는 다른 컴포지트)을 포함할 수 있는 컨테이너 역할을 한다.
    • e.g. “초기 게임 세트”라는 세트 아이템이 도란검 하나와 체력물약 두 개로 구성되어 있다면,
    • 이 세트는 컴포지트 클래스의 인스턴스로 표현됩니다.
    • getPrice() 메서드는 포함된 모든 아이템의 가격을 합산하여 반환합니다.

클라이언트는 개별 아이템과 세트 아이템을 구분하지 않고 동일한 방식으로 처리할 수 있다.

  • e.g. 아이템의 총 가격을 계산하거나, 아이템을 구매하는 등의 작업을
    • 단일 아이템세트 아이템에 대해 동일한 메서드 호출로 수행할 수 있습니다.
  • 컴포지트 패턴은 게임 아이템 관리뿐만 아니라, 파일 시스템의 폴더와 파일 관리 등 다양한 분야에서 유용하게 사용됩니다.

3.2 예시 - 기존

1
// Bag.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;
3
4
import java.util.ArrayList;
5
import java.util.List;
6
7
public class Bag {
8
9
private List<Item> items = new ArrayList<>();
10
11
public void add(Item item) {
12
items.add(item);
13
}
14
15
public List<Item> getItems() {
16
return items;
17
}
18
}
1
// Item.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;
3
4
public class Item {
5
6
private String name;
7
8
private int price;
9
10
public Item(String name, int price) {
11
this.name = name;
12
this.price = price;
13
}
14
15
public int getPrice() {
16
return this.price;
17
}
18
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;
3
4
import java.util.stream.Collectors;
5
6
public class Client {
7
8
public static void main(String[] args) {
9
Item doranBlade = new Item("도란검", 450);
10
Item healPotion = new Item("체력 물약", 50);
11
12
Bag bag = new Bag();
13
bag.add(doranBlade);
14
bag.add(healPotion);
15
16
Client client = new Client();
17
client.printPrice(doranBlade);
18
client.printPrice(bag);
19
}
20
21
private void printPrice(Item item) {
22
System.out.println(item.getPrice());
23
}
24
25
private void printPrice(Bag bag) {
26
int sum = bag.getItems().stream().mapToInt(Item::getPrice).sum();
27
System.out.println(sum);
28
}
29
}

3.3 예시 - 변경

1
// Component.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;
3
4
public interface Component {
5
int getPrice();
6
}
1
// Item.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;
3
4
public class Item implements Component {
5
6
private String name;
7
8
private int price;
9
10
public Item(String name, int price) {
11
this.name = name;
12
this.price = price;
13
}
14
15
@Override
16
public int getPrice() {
17
return this.price;
18
}
19
}
1
// Character.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;
3
4
public class Character implements Component {
5
6
private Bag bag;
7
8
@Override
9
public int getPrice() {
10
return bag.getPrice();
11
}
12
}
1
// Bag.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;
3
4
import java.util.ArrayList;
5
import java.util.List;
6
import java.util.stream.IntStream;
7
8
public class Bag implements Component {
9
10
private List<Component> components = new ArrayList<>();
11
12
public void add(Component component) {
13
components.add(component);
14
}
15
16
public List<Component> getComponents() {
17
return components;
18
}
19
20
@Override
21
public int getPrice() {
22
return components.stream().mapToInt(Component::getPrice).sum();
23
}
24
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
Item doranBlade = new Item("도란검", 450);
8
Item healPotion = new Item("체력 물약", 50);
9
10
Bag bag = new Bag();
11
bag.add(doranBlade);
12
bag.add(healPotion);
13
14
Client client = new Client();
15
client.printPrice(doranBlade);
16
client.printPrice(bag);
17
}
18
19
private void printPrice(Component component) {
20
System.out.println(component.getPrice());
21
}
22
}

3.4 장단점

  • 장점
    • 복잡한 트리 구조를 편리하게 사용할 수 있다.
    • 다형성과 재귀를 활용할 수 있다.
    • 클라이언트 코드를 변경하지 않고, 새로운 엘리먼트 타입을 추가할 수 있다.
  • 단점
    • 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에)
      • 지나치게 일반화 해야 하는 경우도 생길 수 있

3.5 실무에서 어떻게 쓰이나?

자바

  • Swing 라이브러리
  • JSF (JavaServer Faces) 라이브러리
1
package me.whiteship.designpatterns._02_structural_patterns._08_composite._03_java;
2
3
import javax.swing.*;
4
5
public class SwingExample {
6
7
public static void main(String[] args) {
8
JFrame frame = new JFrame();
9
10
JTextField textField = new JTextField();
11
textField.setBounds(200, 200, 200, 40);
12
frame.add(textField);
13
14
JButton button = new JButton("click");
15
button.setBounds(200, 100, 60, 40);
16
button.addActionListener(e -> textField.setText("Hello Swing"));
17
frame.add(button);
18
19
frame.setSize(600, 400);
20
frame.setLayout(null);
21
frame.setVisible(true);
22
}
23
}

4. 데코레이터(Decorator) 패턴

4.1 패턴 소개

gof_2_7

기존 코드를 변경하지 않고, 부가 기능을 추가하는 패턴

  • 상속이 아닌 위임을 사용해서, 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하다.
  • 어떤 컴퓨터 관련 문서에서 동적이고, 유연하다는 표현이 있으면, 그건 런타임에 뭔가를 변경할 수 있다는 의미로,
    • 데코레이터 패턴런타임에 기존 코드를 확장하는 방법이다.

gof_2_8

e.g. 댓글을 남기는 서비스가 있다면, 댓글 서비스를 확장해서 새로운 서비스를 만들어 나갈 수 있다.

  • 스팸 걸러주는 기능, 이모티콘 등 댓글을 꾸며주는 기능인 트리밍(trimming) 기능 등을 추가
  • 또는 나중에 광고를 제거해주는 기능을 추가
  • 이떄부터 상속이 문제가 있단 것을 알게 되는데, 스팸 필터링, 트리밍 코멘트 서비스를
    • 둘 다 상속받아 하나로 만들고 싶지만, 대부분의 프로그래밍 언어에서는 다중 상속을 허용하지 않는다.

e.g. 실생활에서 빵집에서 빵을 만들고 있다.

  • 빵에 초콜릿을 바르면 초콜릿 케이크, 치즈를 바르면 치즈케이크, 과일을 많이 올려놓으면 과일 케이크가 된다.
  • 이처럼 기본 토대에서 decorate(장식, 포장)하는 패턴을 데코레이터 패턴이라 부른다.

4.2 예시 - 기존

1
// CommentService.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;
3
4
public class CommentService {
5
6
public void addComment(String comment) {
7
System.out.println(comment);
8
}
9
}
1
// SpamFilteringCommentService.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;
3
4
public class SpamFilteringCommentService extends CommentService {
5
6
@Override
7
public void addComment(String comment) {
8
boolean isSpam = isSpam(comment);
9
if (!isSpam) {
10
super.addComment(comment);
11
}
12
}
13
14
private boolean isSpam(String comment) {
15
return comment.contains("http");
16
}
17
}
1
// TrimmingCommentService.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;
3
4
public class TrimmingCommentService extends CommentService {
5
6
@Override
7
public void addComment(String comment) {
8
super.addComment(trim(comment));
9
}
10
11
private String trim(String comment) {
12
return comment.replace("...", "");
13
}
14
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;
3
4
public class Client {
5
6
private CommentService commentService;
7
8
public Client(CommentService commentService) {
9
this.commentService = commentService;
10
}
11
12
private void writeComment(String comment) {
13
commentService.addComment(comment);
14
}
15
16
public static void main(String[] args) {
17
Client client = new Client(new SpamFilteringCommentService());
18
client.writeComment("오징어게임");
19
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
20
client.writeComment("http://whiteship.me");
21
}
22
}

4.3 예시 - 변경

1
// CommentService.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public interface CommentService {
5
void addComment(String comment);
6
}
1
// DefaultCommentService.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class DefaultCommentService implements CommentService {
5
6
@Override
7
public void addComment(String comment) {
8
System.out.println(comment);
9
}
10
}
1
// CommentDecorator.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class CommentDecorator implements CommentService {
5
6
private CommentService commentService;
7
8
public CommentDecorator(CommentService commentService) {
9
this.commentService = commentService;
10
}
11
12
@Override
13
public void addComment(String comment) {
14
commentService.addComment(comment);
15
}
16
}
1
// SpamFilteringCommentDecorator.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class SpamFilteringCommentDecorator extends CommentDecorator {
5
6
public SpamFilteringCommentDecorator(CommentService commentService) {
7
super(commentService);
8
}
9
10
@Override
11
public void addComment(String comment) {
12
if (isNotSpam(comment)) {
13
super.addComment(comment);
14
}
15
}
16
17
private boolean isNotSpam(String comment) {
18
return !comment.contains("http");
19
}
20
}
1
// TrimmingCommentDecorator.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class TrimmingCommentDecorator extends CommentDecorator {
5
6
public TrimmingCommentDecorator(CommentService commentService) {
7
super(commentService);
8
}
9
10
@Override
11
public void addComment(String comment) {
12
super.addComment(trim(comment));
13
}
14
15
private String trim(String comment) {
16
return comment.replace("...", "");
17
}
18
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class Client {
5
6
private CommentService commentService;
7
8
public Client(CommentService commentService) {
9
this.commentService = commentService;
10
}
11
12
public void writeComment(String comment) {
13
commentService.addComment(comment);
14
}
15
}
1
// App.java
2
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;
3
4
public class App {
5
6
private static boolean enabledSpamFilter = true;
7
8
private static boolean enabledTrimming = true;
9
10
public static void main(String[] args) {
11
CommentService commentService = new DefaultCommentService();
12
13
if (enabledSpamFilter) {
14
commentService = new SpamFilteringCommentDecorator(commentService);
15
}
16
17
if (enabledTrimming) {
18
commentService = new TrimmingCommentDecorator(commentService);
19
}
20
21
Client client = new Client(commentService);
22
client.writeComment("오징어게임");
23
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
24
client.writeComment("http://whiteship.me");
25
}
26
}

4.4 장단점

  • 장점
    • 새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있다.
    • 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
  • 단점
    • 데코레이터를 조합하는 코드가 복잡할 수 있다

4.5 실무에서는 어떻게 쓰이나?

자바

  • InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
  • java.util.Collections가 제공하는 메소드들 활용한 랩퍼
  • javax.servlet.http.HttpServletRequest/ResponseWrapper
1
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._03_java;
2
3
import java.util.ArrayList;
4
import java.util.Collections;
5
import java.util.List;
6
import javax.servlet.http.HttpServletRequestWrapper;
7
import javax.servlet.http.HttpServletResponseWrapper;
8
9
public class DecoratorInJava {
10
11
public static void main(String[] args) {
12
// Collections가 제공하는 데코레이터 메소드
13
ArrayList list = new ArrayList<>();
14
list.add(new Book());
15
16
List books = Collections.checkedList(list, Book.class);
17
18
// books.add(new Item());
19
20
List unmodifiableList = Collections.unmodifiableList(list);
21
list.add(new Item());
22
unmodifiableList.add(new Book());
23
24
// 서블릿 요청 또는 응답 랩퍼
25
HttpServletRequestWrapper requestWrapper;
26
HttpServletResponseWrapper responseWrapper;
27
}
28
29
private static class Book {}
30
31
private static class Item {}
32
}

스프링

  • ServerHttpRequestDecorator
1
package me.whiteship.designpatterns._02_structural_patterns._09_decorator._03_java;
2
3
import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
4
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
5
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
6
import org.springframework.web.server.WebFilter;
7
8
public class DecoratorInSpring {
9
10
public static void main(String[] args) {
11
// 빈 설정 데코레이터
12
BeanDefinitionDecorator decorator;
13
14
// 웹플럭스 HTTP 요청 /응답 데코레이터
15
ServerHttpRequestDecorator httpRequestDecorator;
16
ServerHttpResponseDecorator httpResponseDecorator;
17
}
18
}

5. 퍼샤드(Facade) 패턴

5.1 패턴 소개

gof_2_9

복잡한 서브 시스템 의존성을 최소화하는 방법.

  • 클라이언트가 사용해야 하는 복잡한 서브 시스템 의존성을 간단한 인터페이스로 추상화할 수 있다.
  • cf. facade는 불어로 ‘건물의 앞쪽 정면(전면)‘이란 뜻이다.
  • 파샤드(Facade) 패턴복잡한 시스템에 대한 간단한 인터페이스를 제공하는 디자인 패턴
    • 이 패턴은 시스템의 복잡성을 숨기고, 클라이언트가 더 쉽게 시스템을 사용할 수 있도록 한다.

gof_2_10

e.g. 이메일을 보내는 과정은 상당히 복잡하다.

  • 이메일을 보내기 위해서는 SMTP 서버 설정, 메시지 포맷팅, 첨부 파일 처리, 메시지 전송 등 여러 단계를 거쳐야 한다.
  • EmailSender 클래스는 sendEmail 메서드를 제공할 수 있다.
    • 클라이언트는 이 메서드에 수신자 주소, 이메일 제목, 본문 내용 등을 파라미터로 전달하기만 하면 된다.
    • sendEmail 메서드 내부에서는 이메일을 보내는 데 필요한 모든 복잡한 과정을 처리한다.
  • 이렇게 파사드 패턴을 사용함으로써, 클라이언트는 이메일 시스템의 내부 구조나 복잡한 과정을 몰라도 간단하게 이메일을 보낼 수 있다.

의존성이 높을 수록 변경하기 어렵고, 그 코드를 테스트하기도 어렵기 때문에, 여러모로 단점이 많다.

  • 가급적 유연하게 느슨한 결합(loosely coupled)을 사용하기 위해 SOLID와 같은 객체지향 원칙이나 디자인 패턴 등을 적용한다.

5.2 예시 - 기존

1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._10_facade._01_before;
3
4
import java.util.Properties;
5
import javax.mail.Message;
6
import javax.mail.MessagingException;
7
import javax.mail.Session;
8
import javax.mail.Transport;
9
import javax.mail.internet.InternetAddress;
10
import javax.mail.internet.MimeMessage;
11
12
public class Client {
13
14
public static void main(String[] args) {
15
String to = "keesun@whiteship.me";
16
String from = "whiteship@whiteship.me";
17
String host = "127.0.0.1";
18
19
Properties properties = System.getProperties();
20
properties.setProperty("mail.smtp.host", host);
21
22
Session session = Session.getDefaultInstance(properties);
23
24
try {
25
MimeMessage message = new MimeMessage(session);
26
message.setFrom(new InternetAddress(from));
27
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
28
message.setSubject("Test Mail from Java Program");
29
message.setText("message");
30
31
Transport.send(message);
32
} catch (MessagingException e) {
33
e.printStackTrace();
34
}
35
}
36
}

5.3 예시 - 변경

1
// EmailMessage.java
2
package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;
3
4
public class EmailMessage {
5
6
private String from;
7
8
private String to;
9
private String cc;
10
private String bcc;
11
12
private String subject;
13
14
private String text;
15
16
public String getFrom() {
17
return from;
18
}
19
20
public void setFrom(String from) {
21
this.from = from;
22
}
23
24
public String getTo() {
25
return to;
26
}
27
28
public void setTo(String to) {
29
this.to = to;
30
}
31
32
public String getSubject() {
33
return subject;
34
}
35
36
public void setSubject(String subject) {
37
this.subject = subject;
38
}
39
40
public String getText() {
41
return text;
42
}
43
44
public void setText(String text) {
45
this.text = text;
46
}
47
48
public String getCc() {
49
return cc;
50
}
51
52
public void setCc(String cc) {
53
this.cc = cc;
54
}
55
56
public String getBcc() {
57
return bcc;
58
}
59
60
public void setBcc(String bcc) {
61
this.bcc = bcc;
62
}
63
}
1
// EmailSettings.java
2
package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;
3
4
public class EmailSettings {
5
6
private String host;
7
8
public String getHost() {
9
return host;
10
}
11
12
public void setHost(String host) {
13
this.host = host;
14
}
15
}
1
// EmailSender.java
2
package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;
3
4
import java.util.Properties;
5
import javax.mail.Message;
6
import javax.mail.MessagingException;
7
import javax.mail.Session;
8
import javax.mail.Transport;
9
import javax.mail.internet.InternetAddress;
10
import javax.mail.internet.MimeMessage;
11
12
public class EmailSender {
13
14
private EmailSettings emailSettings;
15
16
public EmailSender(EmailSettings emailSettings) {
17
this.emailSettings = emailSettings;
18
}
19
20
/**
21
* 이메일 보내는 메소드
22
* @param emailMessage
23
*/
24
public void sendEmail(EmailMessage emailMessage) {
25
Properties properties = System.getProperties();
26
properties.setProperty("mail.smtp.host", emailSettings.getHost());
27
28
Session session = Session.getDefaultInstance(properties);
29
30
try {
31
MimeMessage message = new MimeMessage(session);
32
message.setFrom(new InternetAddress(emailMessage.getFrom()));
33
message.addRecipient(
34
Message.RecipientType.TO,
35
new InternetAddress(emailMessage.getTo())
36
);
37
message.addRecipient(
38
Message.RecipientType.CC,
39
new InternetAddress(emailMessage.getCc())
40
);
41
message.setSubject(emailMessage.getSubject());
42
message.setText(emailMessage.getText());
43
44
Transport.send(message);
45
} catch (MessagingException e) {
46
e.printStackTrace();
47
}
48
}
49
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
EmailSettings emailSettings = new EmailSettings();
8
emailSettings.setHost("127.0.0.1");
9
10
EmailSender emailSender = new EmailSender(emailSettings);
11
12
EmailMessage emailMessage = new EmailMessage();
13
emailMessage.setFrom("keesun");
14
emailMessage.setTo("whiteship");
15
emailMessage.setCc("일남");
16
emailMessage.setSubject("오징어게임");
17
emailMessage.setText("밖은 더 지옥이더라고..");
18
19
emailSender.sendEmail(emailMessage);
20
}
21
}

5.4 장단점

  • 장점 : 서브 시스템에 대한 의존성을 한곳으로 모을 수 있다.
  • 단점 : 퍼사드 클래스가 서브 시스템에 대한 모든 의존성을 가지게 된다

5.5 실무에서는 어떻게 쓰이나?

스프링

  • Spring MVC
  • 스프링이 제공하는 대부분의 기술 독립적인 인터페이스와 그 구현체
1
package me.whiteship.designpatterns._02_structural_patterns._10_facade._03_java;
2
3
import org.springframework.jdbc.support.JdbcTransactionManager;
4
import org.springframework.mail.MailSender;
5
import org.springframework.mail.javamail.JavaMailSenderImpl;
6
import org.springframework.transaction.PlatformTransactionManager;
7
8
public class FacadeInSpring {
9
10
public static void main(String[] args) {
11
MailSender mailSender = new JavaMailSenderImpl();
12
13
PlatformTransactionManager platformTransactionManager = new JdbcTransactionManager();
14
}
15
}

6. 플라이웨이트(Flyweight) 패턴

6.1 패턴 소개

gof_2_11

객체를 가볍게 만들어 메모리 사용을 줄이는 패턴.

  • 자주 변하는 속성(또는 외적인 속성, extrinsit)변하지 않는 속성(또는 내적인 속성, intrinsit)을 분리하고,
    • 재사용하여 메모리 사용을 줄일 수 있다.
  • 애플리케이션에서 굉장히 많은 인스턴스를 만드는 특징을 가지고 있는 애플리케이션에서 주로 사용한다.
  • 많은 인스턴스를 만들다보면, 메모리 사용을 많이 하게 되서, 메모리가 부족해지는 현상이 발생한다.
    • flyweight 패턴을 적용해, 공통되는 부분을 따로 모아서 재사용하는 패턴이다.

즉, 자주 변하는 속성(또는 외적인 속성, extrinsit)자주 변하지 않는 속성(또는 내적인 속성, intrinsit)을 분리해서 자주 변하지 않는 속성을 재사용하는 방법이다.

  • cf. flyweight는 ‘가벼운‘이란 뜻으로, 복싱에서 체급을 나눌 떄, flywight라는 체급이 있다.

gof_2_12

e.g. 문서에 수천 개의 글자가 있고, 모든 글자가 동일한 폰트 스타일을 사용한다면,

  • 각 글자마다 폰트 스타일 정보를 별도로 저장하는 것은 메모리 낭비가 될 수 있다.
  • 특정 폰트 스타일(예: 글꼴, 크기, 색상 등)과 같이 반복되는 정보를 공유 객체로 만들어 메모리 사용을 줄일 수 있다.
  • 결과적으로 문서 내의 모든 텍스트가 동일한 폰트 스타일을 사용할 경우,
    • 단 하나의 폰트 스타일 객체만 메모리에 존재하게 된다.

6.2 예시 - 기존

1
// Character.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._01_before;
3
4
public class Character {
5
6
private char value;
7
8
private String color;
9
10
private String fontFamily;
11
12
private int fontSize;
13
14
public Character(char value, String color, String fontFamily, int fontSize) {
15
this.value = value;
16
this.color = color;
17
this.fontFamily = fontFamily;
18
this.fontSize = fontSize;
19
}
20
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._01_before;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
Character c1 = new Character('h', "white", "Nanum", 12);
8
Character c2 = new Character('e', "white", "Nanum", 12);
9
Character c3 = new Character('l', "white", "Nanum", 12);
10
Character c4 = new Character('l', "white", "Nanum", 12);
11
Character c5 = new Character('o', "white", "Nanum", 12);
12
}
13
}

6.3 예시 - 변경

1
// Font.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;
3
4
public final class Font {
5
6
final String family;
7
8
final int size;
9
10
public Font(String family, int size) {
11
this.family = family;
12
this.size = size;
13
}
14
15
public String getFamily() {
16
return family;
17
}
18
19
public int getSize() {
20
return size;
21
}
22
}
1
// FontFactory.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;
3
4
import java.util.HashMap;
5
import java.util.Map;
6
7
public class FontFactory {
8
9
private Map<String, Font> cache = new HashMap<>();
10
11
public Font getFont(String font) {
12
if (cache.containsKey(font)) {
13
return cache.get(font);
14
} else {
15
String[] split = font.split(":");
16
Font newFont = new Font(split[0], Integer.parseInt(split[1]));
17
cache.put(font, newFont);
18
return newFont;
19
}
20
}
21
}
1
// Character.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;
3
4
public class Character {
5
6
private char value;
7
8
private String color;
9
10
private Font font;
11
12
public Character(char value, String color, Font font) {
13
this.value = value;
14
this.color = color;
15
this.font = font;
16
}
17
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
FontFactory fontFactory = new FontFactory();
8
Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
9
Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
10
Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
11
}
12
}

6.4 장단점

  • 장점 : 애플리케이션에서 사용하는 메모리를 줄일 수 있다.
  • 단점 : 코드의 복잡도가 증가한다.

6.5 실무에서는 어떻게 쓰이나?

자바

1
// FlyweightInJava.java
2
package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._03_java;
3
4
public class FlyweightInJava {
5
6
public static void main(String[] args) {
7
Integer i1 = Integer.valueOf(10);
8
Integer i2 = Integer.valueOf(10);
9
System.out.println(i1 == i2);
10
}
11
}

7. 프록시(Proxy) 패턴

7.1 패턴 소개

gof_2_13

특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴.

  • 초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용 할 수 있다.
  • 특정 객체의 어떤 오퍼레이션들을 접근하기 전에, 이 프록시 객체를 먼저 지나서 접근하는 패턴이다
  • cf. proxy가 사전적으로 ‘대리, 대리인’이라는 뜻
  • 쉽게말해, 클라이언트가 원래 사용하려는 객체를 직접 쓰는게 아니라, 대리인을 거쳐서 쓰는 패턴이다.

gof_2_14

cf. 클라이언트가 게임 서비스를 사용해서 게임을 시작한다.

  • 게임 클라이언트가 게임 서버에 직접 접근하는 대신, Proxy 서버를 통해 접근한다.
  • 이 Proxy 서버는 클라이언트의 요청을 검증하고, 적절한 경우에만 게임 서버로 요청을 전달한다.

7.2 예시 - 기존

1
// GameService.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._01_before;
3
4
public class GameService {
5
6
public void startGame() {
7
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
8
}
9
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._01_before;
3
4
public class Client {
5
6
public static void main(String[] args) throws InterruptedException {
7
GameService gameService = new GameService();
8
gameService.startGame();
9
}
10
}

7.3 예시 - 변경(상속 사용)

기존을 코드를 변경하지 않고 적용하는 방법

1
// GameService.java
2
public class GameService {
3
4
public void startGame() throws InterruptedException {
5
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
6
Thread.sleep(1000L);
7
}
8
9
}
1
// GameServiceProxy.java
2
public class GameServiceProxy extends GameService {
3
4
@Override
5
public void startGame() throws InterruptedException {
6
long before = System.currentTimeMillis();
7
super.startGame();
8
System.out.println(System.currentTimeMillis() - before);
9
}
10
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
GameService gameService = new GameServiceProxy();
8
gameService.startGame();
9
}
10
}

7.4 예시 - 변경(인터페이스 사용)

1
// GameService.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;
3
4
public interface GameService {
5
void startGame();
6
}
1
// DefaultGameService.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;
3
4
public class DefaultGameService implements GameService {
5
6
@Override
7
public void startGame() {
8
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
9
}
10
}
1
// GameServiceProxy.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;
3
4
public class GameServiceProxy implements GameService {
5
6
private GameService gameService;
7
8
@Override
9
public void startGame() {
10
long before = System.currentTimeMillis();
11
if (this.gameService == null) {
12
this.gameService = new DefaultGameService();
13
}
14
15
gameService.startGame();
16
System.out.println(System.currentTimeMillis() - before);
17
}
18
}
1
// Client.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;
3
4
public class Client {
5
6
public static void main(String[] args) {
7
GameService gameService = new GameServiceProxy();
8
gameService.startGame();
9
}
10
}

7.5 장단점

  • 장점
    • 기존 코드를 변경하지 않고, 새로운 기능을 추가할 수 있다.
    • 기존 코드가 해야 하는 일만 유지할 수 있다.
    • 기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다.
  • 단점 : 코드의 복잡도가 증가한다

7.6 실무에서 어떻게 쓰이나?

특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴.

  • 자바 : 다이나믹 프록시, java.lang.reflect.Proxy
1
// ProxyInJava.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;
3
4
import java.lang.reflect.Proxy;
5
import me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after.DefaultGameService;
6
import me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after.GameService;
7
8
public class ProxyInJava {
9
10
public static void main(String[] args) {
11
ProxyInJava proxyInJava = new ProxyInJava();
12
proxyInJava.dynamicProxy();
13
}
14
15
private void dynamicProxy() {
16
GameService gameServiceProxy = getGameServiceProxy(
17
new DefaultGameService()
18
);
19
gameServiceProxy.startGame();
20
}
21
22
private GameService getGameServiceProxy(GameService target) {
23
return (GameService) Proxy.newProxyInstance(
24
this.getClass().getClassLoader(),
25
new Class[] { GameService.class },
26
(proxy, method, args) -> {
27
System.out.println("O");
28
method.invoke(target, args);
29
System.out.println("ㅁ");
30
return null;
31
}
32
);
33
}
34
}
  • 스프링 : 스프링 AOP
1
// PerfAspect.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;
3
4
import org.aspectj.lang.ProceedingJoinPoint;
5
import org.aspectj.lang.annotation.Around;
6
import org.aspectj.lang.annotation.Aspect;
7
import org.springframework.stereotype.Component;
8
9
@Aspect
10
@Component
11
public class PerfAspect {
12
13
@Around("bean(gameService)")
14
public void timestamp(ProceedingJoinPoint point) throws Throwable {
15
long before = System.currentTimeMillis();
16
point.proceed();
17
System.out.println(System.currentTimeMillis() - before);
18
}
19
}
1
// GameService.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;
3
4
import org.springframework.cache.annotation.Cacheable;
5
import org.springframework.stereotype.Service;
6
import org.springframework.transaction.annotation.Transactional;
7
8
@Service
9
public class GameService {
10
11
public void startGame() {
12
System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
13
}
14
}
1
// App.java
2
package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;
3
4
import org.springframework.boot.ApplicationRunner;
5
import org.springframework.boot.SpringApplication;
6
import org.springframework.boot.WebApplicationType;
7
import org.springframework.boot.autoconfigure.SpringBootApplication;
8
import org.springframework.context.annotation.Bean;
9
10
@SpringBootApplication
11
public class App {
12
13
public static void main(String[] args) {
14
SpringApplication app = new SpringApplication(App.class);
15
app.setWebApplicationType(WebApplicationType.NONE);
16
app.run(args);
17
}
18
19
@Bean
20
public ApplicationRunner applicationRunner(GameService gameService) {
21
return args -> {
22
gameService.startGame();
23
};
24
}
25
}