1. 어댑터(Adapter) 패턴
1.1 패턴 소개

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

e.g. 110v용 콘센트를 220v용 콘센트에 꽂는다던가, 반대로 220v용 콘센트를 110v용 콘센트에 꽂을 때,
- 이 중간에 쓰이는
어댑터(돼지코)가 바로 일상에서 볼 수 있는 어댑터 패턴이다.
1.2 예시 - 기존
1// UserDetails.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;34public interface UserDetails {56String getUsername();78String getPassword();910}
1// UserDetailsService.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;34public interface UserDetailsService {56UserDetails loadUser(String username);78}
1// LoginHandler.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;34public class LoginHandler {56UserDetailsService userDetailsService;78public LoginHandler(UserDetailsService userDetailsService) {9this.userDetailsService = userDetailsService;10}1112public String login(String username, String password) {13UserDetails userDetails = userDetailsService.loadUser(username);14if (userDetails.getPassword().equals(password)) {15return userDetails.getUsername();16} else {17throw new IllegalArgumentException();18}19}20}
1// Account.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;34public class Account {56private String name;78private String password;910private String email;1112public String getName() {13return name;14}1516public void setName(String name) {17this.name = name;18}1920public String getPassword() {21return password;22}2324public void setPassword(String password) {25this.password = password;26}2728public String getEmail() {29return email;30}3132public void setEmail(String email) {33this.email = email;34}3536}
1// AccountService.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;34public class AccountService {56public Account findAccountByUsername(String username) {7Account account = new Account();8account.setName(username);9account.setPassword(username);10account.setEmail(username);11return account;12}1314public void createNewAccount(Account account) {1516}1718public void updateAccount(Account account) {1920}2122}
1.3 예시 - 변경
1// AccountUserDetails.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;34import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetails;56public class AccountUserDetails implements UserDetails {78private Account account;910public AccountUserDetails(Account account) {11this.account = account;12}1314@Override15public String getUsername() {16return account.getName();17}1819@Override20public String getPassword() {21return account.getPassword();22}23}
1// AccountUserDetailsService.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;34import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetails;5import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetailsService;67public class AccountUserDetailsService implements UserDetailsService {89private AccountService accountService;1011public AccountUserDetailsService(AccountService accountService) {12this.accountService = accountService;13}1415@Override16public UserDetails loadUser(String username) {17return new AccountUserDetails(accountService.findAccountByUsername(username));18}19}
1// App.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after;34import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.LoginHandler;5import me.whiteship.designpatterns._02_structural_patterns._06_adapter._02_after.security.UserDetailsService;67public class App {89public static void main(String[] args) {10AccountService accountService = new AccountService();11UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);12LoginHandler loginHandler = new LoginHandler(userDetailsService);13String login = loginHandler.login("keesun", "keesun");14System.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.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._03_java;34import java.io.*;5import java.util.*;67public class AdapterInJava {89public static void main(String[] args) {10// collections11List<String> strings = Arrays.asList("a", "b", "c");12Enumeration<String> enumeration = Collections.enumeration(strings);13ArrayList<String> list = Collections.list(enumeration);1415// io16try (17InputStream is = new FileInputStream("input.txt");18InputStreamReader isr = new InputStreamReader(is);19BufferedReader reader = new BufferedReader(isr)20) {21while (reader.ready()) {22System.out.println(reader.readLine());23}24} catch (IOException e) {25throw new RuntimeException(e);26}27}28}
스프링
- HandlerAdpter: 우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행할 수 있는 형태로 변환해주는 어댑터용 인터페이스
1// AdapterInSpring.java2package me.whiteship.designpatterns._02_structural_patterns._06_adapter._03_java;34import org.springframework.web.servlet.DispatcherServlet;5import org.springframework.web.servlet.HandlerAdapter;6import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;78public class AdapterInSpring {910public static void main(String[] args) {11DispatcherServlet dispatcherServlet = new DispatcherServlet();12HandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();13}14}
2. 브릿지(Bridge) 패턴
2.1 패턴 소개

추상적인 것과 구체적인 것을 분리하여 연결하는 패턴
-
하나의 계층 구조일 때 보다 각기 나누었을 때 독립적인 계층 구조로 발전 시킬 수 있다.
-
어댑터 패턴이 서로 상이한 두 인터페이스를 연결하는 것이라면,브릿지 패턴은 추상적인 것과 구체적인 것 사이를 연결하는 브릿지를 만들기 위해 상속이 아닌, 컴포지트를 사용- 그래서 나눠서 연결한다는 의미로 bridge(다리)를 사용한다.
-
위 그림의 중간
Abstration은 추상적인 부분, 위 그림의 가장 오른쪽Implementation은 구체적인 부분이다.- 여기서 중요한 건 추상적인 부분과 구체적인 부분을 연결해주는
다리 역할을 하는 화살표이다. - 어떤 동작들만 있는 것, 상태만 모아놓은 어떤 프론트엔드, 백엔드만 모아놓은 것과 같이
- 서로 성격이 상이한 것들을 분리해서, 그것들을 대칭 구조가 아닌 서로 분리하고, 그 둘 사이를 연결하는 다리를 놓아주는 것
- 여기서 중요한 건 추상적인 부분과 구체적인 부분을 연결해주는

e.g. 게임 롤 캐릭터인 아리와 아칼리가 있고, 그들의 스킨인 수영복과 KDA 스킨이 있다.
- 챔피언은 게임 내에서 플레이어가 조종하는 주요 캐릭터이고,
- 스킨은 챔피언의 외모를 변경하지만, 챔피언의 기본 능력이나 게임 플레이에는 영향을 주지 않는다.
- 브릿지 패턴을 이용하여 이를 모델링한다면, “챔피언”을 추상화로, “스킨”을 구현으로 볼 수 있다.
- 이렇게 하면,
챔피언(아리, 아칼리)과스킨(수영복, KDA)을 독립적으로 확장할 수 있다.
- 추상화(Abstraction) 층: 이 층에서는 챔피언의 추상적인 개념을 정의.
- e.g. 챔피언 클래스는 모든 챔피언이 공통으로 가지고 있는 특성(예: 이름, 기본 공격)을 정의할 수 있다.
- 그리고 이 추상화 층에서는 스킨을 적용하는 메소드를 포함할 수 있다.
- 구현(Implementation) 층: 이 층에서는 스킨의 구체적인 구현을 정의
- 수영복 스킨, KDA 스킨 등 각각의 스킨은 이 구현 층에서 정의된다.
- 각 스킨은 챔피언의 외모를 변경하는 방법을 구체적으로 구현한다.
이러한 구조를 통해, 새로운 챔피언이나 스킨을 추가할 때 기존 코드를 변경하지 않고도 확장이 가능하다.
- e.g. 새로운 챔피언을 추가하려면,
추상화 층에 새로운 챔피언 클래스를 추가하기만 하면 되고, - e.g. 새로운 스킨을 추가하려면,
구현 층에 새로운 스킨 클래스를 추가하면 된다.
브릿지 패턴을 사용함으로써, 챔피언과 스킨의 결합도를 낮추고, 각각을 독립적으로 확장할 수 있는 유연성을 확보할 수 있다
2.2 예시 - 기존
1// Skin.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34public interface Skin {5String getName();6}
1// Champion.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;34import me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after.Skin;56public interface Champion extends Skin {7void move();89void skillQ();1011void skillW();1213void skillE();1415void skillR();16}
1// KDA아리.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;34public class KDA아리 implements Champion {56@Override7public void move() {8System.out.println("KDA 아리 move");9}1011@Override12public void skillQ() {13System.out.println("KDA 아리 Q");14}1516@Override17public void skillW() {18System.out.println("KDA 아리 W");19}2021@Override22public void skillE() {23System.out.println("KDA 아리 E");24}2526@Override27public void skillR() {28System.out.println("KDA 아리 R");29}3031@Override32public String getName() {33return null;34}35}
1// PoolParty아리.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;34public class PoolParty아리 implements Champion {56@Override7public void move() {8System.out.println("PoolParty move");9}1011@Override12public void skillQ() {13System.out.println("PoolParty Q");14}1516@Override17public void skillW() {18System.out.println("PoolParty W");19}2021@Override22public void skillE() {23System.out.println("PoolParty E");24}2526@Override27public void skillR() {28System.out.println("PoolParty R");29}3031@Override32public String getName() {33return null;34}35}
1// App.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before;34public class App {56public static void main(String[] args) {7Champion kda아리 = new KDA아리();8kda아리.skillQ();9kda아리.skillR();10}11}
2.3 예시 - 변경
1// DefaultChampion.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34import me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before.Champion;56public class DefaultChampion implements Champion {78private Skin skin;910private String name;1112public DefaultChampion(Skin skin, String name) {13this.skin = skin;14this.name = name;15}1617@Override18public void move() {19System.out.printf("%s %s move\n", skin.getName(), this.name);20}2122@Override23public void skillQ() {24System.out.printf("%s %s Q\n", skin.getName(), this.name);25}2627@Override28public void skillW() {29System.out.printf("%s %s W\n", skin.getName(), this.name);30}3132@Override33public void skillE() {34System.out.printf("%s %s E\n", skin.getName(), this.name);35}3637@Override38public void skillR() {39System.out.printf("%s %s R\n", skin.getName(), this.name);40}4142@Override43public String getName() {44return null;45}46}
1// KDA.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34public class KDA implements Skin {56@Override7public String getName() {8return "KDA";9}10}
1// PoolParty.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34public class PoolParty implements Skin {56@Override7public String getName() {8return "PoolParty";9}10}
1// 아리.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34public class 아리 extends DefaultChampion {56public 아리(Skin skin) {7super(skin, "아리");8}9}
1// 아칼리.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34public class 아칼리 extends DefaultChampion {56public 아칼리(Skin skin) {7super(skin, "아칼리");8}9}
1// App.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._02_after;34import me.whiteship.designpatterns._02_structural_patterns._07_bridge._01_before.Champion;56public abstract class App implements Champion {78public static void main(String[] args) {9Champion kda아리 = new 아리(new KDA());10kda아리.skillQ();11kda아리.skillW();1213Champion poolParty아리 = new 아리(new PoolParty());14poolParty아리.skillR();15poolParty아리.skillW();16}17}
2.4 장단점
- 장점
추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있다.추상적인 코드과구체적인 코드를 분리하여 수 있다.
- 단점
- 계층 구조가 늘어나 복잡도가 증가할 수 있다
2.5 실무에서 어떻게 쓰이나?
자바
- JDBC API, DriverManger와 Driver
1// JdbcExample.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;34import java.sql.*;56public class JdbcExample {78public static void main(String[] args) throws ClassNotFoundException {9Class.forName("org.h2.Driver");1011try (12Connection conn = DriverManager.getConnection(13"jdbc:h2:mem:~/test",14"sa",15""16)17) {18String sql =19"CREATE TABLE ACCOUNT " +20"(id INTEGER not NULL, " +21" email VARCHAR(255), " +22" password VARCHAR(255), " +23" PRIMARY KEY ( id ))";2425Statement statement = conn.createStatement();26statement.execute(sql);27// PreparedStatement statement1 = conn.prepareStatement(sql);28// ResultSet resultSet = statement.executeQuery(sql);29} catch (SQLException e) {30throw new RuntimeException(e);31}32}33}
Java의 SLF4J, 로깅 퍼사드와 로거
1// Slf4jExample.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;34import org.slf4j.Logger;5import org.slf4j.LoggerFactory;67public class Slf4jExample {89private static Logger logger = LoggerFactory.getLogger(Slf4jExample.class);1011public static void main(String[] args) {12logger.info("hello logger");13}14}
스프링의 Portable Service Abstraction
1// BridgeInSpring.java2package me.whiteship.designpatterns._02_structural_patterns._07_bridge._03_java;34import org.springframework.jdbc.support.JdbcTransactionManager;5import org.springframework.mail.MailSender;6import org.springframework.mail.javamail.JavaMailSenderImpl;7import org.springframework.transaction.PlatformTransactionManager;89public class BridgeInSpring {1011public static void main(String[] args) {12MailSender mailSender = new JavaMailSenderImpl();1314PlatformTransactionManager platformTransactionManager = new JdbcTransactionManager();15}16}
3. 컴포짓(Composite) 패턴
3.1 패턴 소개

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

e.g. 게임에서 도란검(450원), 체력물약(50원)이 있다. 그리고 이 아이템들을 가방에 넣어놨다.
- 게임에서 아이템은 단일 아이템일 수도 있고, 여러 아이템이 조합된 세트 아이템일 수도 있습니다.
- 클라이언트 입장에서는 아이템의 가격을 개별로 출력하고 싶을 떄도 있고,
- 반대로 가방 전체에 들어있는 아이템 가격을 알고 싶을 떄도 있을 것이다.
- 아이템 가격은 그냥 출력하면 되는데, 가방의 아이템은 모든 아이템들을 순회하면서 출력해야 한다.
- 이러면 가방의 모든 값을 구하는 로직이 클라이언트 측에 남는데, 이것이 객체지향적으로 올바른가에 대한 고민이 생긴다.
- 이는 컴포짓 패턴을 적용하면 이 문제를 쉽게 해결할 수 있다.
- 값을 구할 수 있는 모든 컴포넌트들의 공통 인터페이스를 정의한다.
- 그래서 클라이언트는 컴포넌트라는 인터페이스 타입만 본다.
- 우리가 만들 아이템, 백, 캐릭터 등은 leaf 또는 composite 객체가 된다.
- leaf 객체는 가장 기본적인 단위가 되고,
- 여러 기본적인 단위 타입들을 그룹으로 가져가는 composite 타입의 객체가 있다.
- composite 객체는 여러 개의 컴포넌트들을 배열 또는 리스트로 가지고 있다.
- 하지만 이떄도 타입은 절대 leaf 타입이 아니다. 컴포넌트 타입이다.
- 그래서 컴포짓을 써도 컴포넌트 타입으로 leaf 또는 bag 또는 composite를 참조할 수 있는 것이다.
- 값을 구할 수 있는 모든 컴포넌트들의 공통 인터페이스를 정의한다.
- 컴포넌트(Component) 인터페이스:
- 인터페이스는 개별 아이템(도란검, 체력물약)과 세트 아이템 모두가 구현해야 하는 공통의 메서드를 정의
- e.g.
getPrice()메서드는 아이템의 가격을 반환.- 리프(Leaf) 클래스:
- 이 클래스는 개별 아이템을 나타낸다.
- 도란검과 체력물약은 각각 리프 클래스의 인스턴스로 표현될 수 있으며,
getPrice()메서드를 통해 자신의 가격을 반환한다.- 컴포지트(Composite) 클래스:
- 이 클래스는 여러 개의 아이템(리프 또는 다른 컴포지트)을 포함할 수 있는 컨테이너 역할을 한다.
- e.g. “초기 게임 세트”라는 세트 아이템이 도란검 하나와 체력물약 두 개로 구성되어 있다면,
- 이 세트는 컴포지트 클래스의 인스턴스로 표현됩니다.
getPrice()메서드는 포함된 모든 아이템의 가격을 합산하여 반환합니다.
클라이언트는 개별 아이템과 세트 아이템을 구분하지 않고 동일한 방식으로 처리할 수 있다.
- e.g. 아이템의 총 가격을 계산하거나, 아이템을 구매하는 등의 작업을
단일 아이템과세트 아이템에 대해 동일한 메서드 호출로 수행할 수 있습니다.
- 컴포지트 패턴은 게임 아이템 관리뿐만 아니라, 파일 시스템의 폴더와 파일 관리 등 다양한 분야에서 유용하게 사용됩니다.
3.2 예시 - 기존
1// Bag.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;34import java.util.ArrayList;5import java.util.List;67public class Bag {89private List<Item> items = new ArrayList<>();1011public void add(Item item) {12items.add(item);13}1415public List<Item> getItems() {16return items;17}18}
1// Item.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;34public class Item {56private String name;78private int price;910public Item(String name, int price) {11this.name = name;12this.price = price;13}1415public int getPrice() {16return this.price;17}18}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._01_before;34import java.util.stream.Collectors;56public class Client {78public static void main(String[] args) {9Item doranBlade = new Item("도란검", 450);10Item healPotion = new Item("체력 물약", 50);1112Bag bag = new Bag();13bag.add(doranBlade);14bag.add(healPotion);1516Client client = new Client();17client.printPrice(doranBlade);18client.printPrice(bag);19}2021private void printPrice(Item item) {22System.out.println(item.getPrice());23}2425private void printPrice(Bag bag) {26int sum = bag.getItems().stream().mapToInt(Item::getPrice).sum();27System.out.println(sum);28}29}
3.3 예시 - 변경
1// Component.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;34public interface Component {5int getPrice();6}
1// Item.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;34public class Item implements Component {56private String name;78private int price;910public Item(String name, int price) {11this.name = name;12this.price = price;13}1415@Override16public int getPrice() {17return this.price;18}19}
1// Character.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;34public class Character implements Component {56private Bag bag;78@Override9public int getPrice() {10return bag.getPrice();11}12}
1// Bag.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;34import java.util.ArrayList;5import java.util.List;6import java.util.stream.IntStream;78public class Bag implements Component {910private List<Component> components = new ArrayList<>();1112public void add(Component component) {13components.add(component);14}1516public List<Component> getComponents() {17return components;18}1920@Override21public int getPrice() {22return components.stream().mapToInt(Component::getPrice).sum();23}24}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._08_composite._02_after;34public class Client {56public static void main(String[] args) {7Item doranBlade = new Item("도란검", 450);8Item healPotion = new Item("체력 물약", 50);910Bag bag = new Bag();11bag.add(doranBlade);12bag.add(healPotion);1314Client client = new Client();15client.printPrice(doranBlade);16client.printPrice(bag);17}1819private void printPrice(Component component) {20System.out.println(component.getPrice());21}22}
3.4 장단점
- 장점
- 복잡한 트리 구조를 편리하게 사용할 수 있다.
- 다형성과 재귀를 활용할 수 있다.
- 클라이언트 코드를 변경하지 않고, 새로운 엘리먼트 타입을 추가할 수 있다.
- 단점
- 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에)
- 지나치게 일반화 해야 하는 경우도 생길 수 있다
- 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에)
3.5 실무에서 어떻게 쓰이나?
자바
- Swing 라이브러리
- JSF (JavaServer Faces) 라이브러리
1package me.whiteship.designpatterns._02_structural_patterns._08_composite._03_java;23import javax.swing.*;45public class SwingExample {67public static void main(String[] args) {8JFrame frame = new JFrame();910JTextField textField = new JTextField();11textField.setBounds(200, 200, 200, 40);12frame.add(textField);1314JButton button = new JButton("click");15button.setBounds(200, 100, 60, 40);16button.addActionListener(e -> textField.setText("Hello Swing"));17frame.add(button);1819frame.setSize(600, 400);20frame.setLayout(null);21frame.setVisible(true);22}23}
4. 데코레이터(Decorator) 패턴
4.1 패턴 소개

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

e.g. 댓글을 남기는 서비스가 있다면, 댓글 서비스를 확장해서 새로운 서비스를 만들어 나갈 수 있다.
- 스팸 걸러주는 기능, 이모티콘 등 댓글을 꾸며주는 기능인 트리밍(trimming) 기능 등을 추가
- 또는 나중에 광고를 제거해주는 기능을 추가
- 이떄부터 상속이 문제가 있단 것을 알게 되는데, 스팸 필터링, 트리밍 코멘트 서비스를
- 둘 다 상속받아 하나로 만들고 싶지만, 대부분의 프로그래밍 언어에서는 다중 상속을 허용하지 않는다.
e.g. 실생활에서 빵집에서 빵을 만들고 있다.
- 빵에 초콜릿을 바르면 초콜릿 케이크, 치즈를 바르면 치즈케이크, 과일을 많이 올려놓으면 과일 케이크가 된다.
- 이처럼 기본 토대에서
decorate(장식, 포장)하는 패턴을 데코레이터 패턴이라 부른다.
4.2 예시 - 기존
1// CommentService.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;34public class CommentService {56public void addComment(String comment) {7System.out.println(comment);8}9}
1// SpamFilteringCommentService.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;34public class SpamFilteringCommentService extends CommentService {56@Override7public void addComment(String comment) {8boolean isSpam = isSpam(comment);9if (!isSpam) {10super.addComment(comment);11}12}1314private boolean isSpam(String comment) {15return comment.contains("http");16}17}
1// TrimmingCommentService.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;34public class TrimmingCommentService extends CommentService {56@Override7public void addComment(String comment) {8super.addComment(trim(comment));9}1011private String trim(String comment) {12return comment.replace("...", "");13}14}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._01_before;34public class Client {56private CommentService commentService;78public Client(CommentService commentService) {9this.commentService = commentService;10}1112private void writeComment(String comment) {13commentService.addComment(comment);14}1516public static void main(String[] args) {17Client client = new Client(new SpamFilteringCommentService());18client.writeComment("오징어게임");19client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");20client.writeComment("http://whiteship.me");21}22}
4.3 예시 - 변경
1// CommentService.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public interface CommentService {5void addComment(String comment);6}
1// DefaultCommentService.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class DefaultCommentService implements CommentService {56@Override7public void addComment(String comment) {8System.out.println(comment);9}10}
1// CommentDecorator.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class CommentDecorator implements CommentService {56private CommentService commentService;78public CommentDecorator(CommentService commentService) {9this.commentService = commentService;10}1112@Override13public void addComment(String comment) {14commentService.addComment(comment);15}16}
1// SpamFilteringCommentDecorator.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class SpamFilteringCommentDecorator extends CommentDecorator {56public SpamFilteringCommentDecorator(CommentService commentService) {7super(commentService);8}910@Override11public void addComment(String comment) {12if (isNotSpam(comment)) {13super.addComment(comment);14}15}1617private boolean isNotSpam(String comment) {18return !comment.contains("http");19}20}
1// TrimmingCommentDecorator.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class TrimmingCommentDecorator extends CommentDecorator {56public TrimmingCommentDecorator(CommentService commentService) {7super(commentService);8}910@Override11public void addComment(String comment) {12super.addComment(trim(comment));13}1415private String trim(String comment) {16return comment.replace("...", "");17}18}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class Client {56private CommentService commentService;78public Client(CommentService commentService) {9this.commentService = commentService;10}1112public void writeComment(String comment) {13commentService.addComment(comment);14}15}
1// App.java2package me.whiteship.designpatterns._02_structural_patterns._09_decorator._02_after;34public class App {56private static boolean enabledSpamFilter = true;78private static boolean enabledTrimming = true;910public static void main(String[] args) {11CommentService commentService = new DefaultCommentService();1213if (enabledSpamFilter) {14commentService = new SpamFilteringCommentDecorator(commentService);15}1617if (enabledTrimming) {18commentService = new TrimmingCommentDecorator(commentService);19}2021Client client = new Client(commentService);22client.writeComment("오징어게임");23client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");24client.writeComment("http://whiteship.me");25}26}
4.4 장단점
- 장점
- 새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있다.
- 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
- 단점
- 데코레이터를 조합하는 코드가 복잡할 수 있다
4.5 실무에서는 어떻게 쓰이나?
자바
- InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
- java.util.Collections가 제공하는 메소드들 활용한 랩퍼
- javax.servlet.http.HttpServletRequest/ResponseWrapper
1package me.whiteship.designpatterns._02_structural_patterns._09_decorator._03_java;23import java.util.ArrayList;4import java.util.Collections;5import java.util.List;6import javax.servlet.http.HttpServletRequestWrapper;7import javax.servlet.http.HttpServletResponseWrapper;89public class DecoratorInJava {1011public static void main(String[] args) {12// Collections가 제공하는 데코레이터 메소드13ArrayList list = new ArrayList<>();14list.add(new Book());1516List books = Collections.checkedList(list, Book.class);1718// books.add(new Item());1920List unmodifiableList = Collections.unmodifiableList(list);21list.add(new Item());22unmodifiableList.add(new Book());2324// 서블릿 요청 또는 응답 랩퍼25HttpServletRequestWrapper requestWrapper;26HttpServletResponseWrapper responseWrapper;27}2829private static class Book {}3031private static class Item {}32}
스프링
- ServerHttpRequestDecorator
1package me.whiteship.designpatterns._02_structural_patterns._09_decorator._03_java;23import org.springframework.beans.factory.xml.BeanDefinitionDecorator;4import org.springframework.http.server.reactive.ServerHttpRequestDecorator;5import org.springframework.http.server.reactive.ServerHttpResponseDecorator;6import org.springframework.web.server.WebFilter;78public class DecoratorInSpring {910public static void main(String[] args) {11// 빈 설정 데코레이터12BeanDefinitionDecorator decorator;1314// 웹플럭스 HTTP 요청 /응답 데코레이터15ServerHttpRequestDecorator httpRequestDecorator;16ServerHttpResponseDecorator httpResponseDecorator;17}18}
5. 퍼샤드(Facade) 패턴
5.1 패턴 소개

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

e.g. 이메일을 보내는 과정은 상당히 복잡하다.
- 이메일을 보내기 위해서는 SMTP 서버 설정, 메시지 포맷팅, 첨부 파일 처리, 메시지 전송 등 여러 단계를 거쳐야 한다.
EmailSender클래스는sendEmail메서드를 제공할 수 있다.- 클라이언트는 이 메서드에 수신자 주소, 이메일 제목, 본문 내용 등을 파라미터로 전달하기만 하면 된다.
sendEmail메서드 내부에서는 이메일을 보내는 데 필요한 모든 복잡한 과정을 처리한다.
- 이렇게 파사드 패턴을 사용함으로써, 클라이언트는 이메일 시스템의 내부 구조나 복잡한 과정을 몰라도 간단하게 이메일을 보낼 수 있다.
의존성이 높을 수록 변경하기 어렵고, 그 코드를 테스트하기도 어렵기 때문에, 여러모로 단점이 많다.
- 가급적 유연하게
느슨한 결합(loosely coupled)을 사용하기 위해 SOLID와 같은 객체지향 원칙이나 디자인 패턴 등을 적용한다.
5.2 예시 - 기존
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._10_facade._01_before;34import java.util.Properties;5import javax.mail.Message;6import javax.mail.MessagingException;7import javax.mail.Session;8import javax.mail.Transport;9import javax.mail.internet.InternetAddress;10import javax.mail.internet.MimeMessage;1112public class Client {1314public static void main(String[] args) {15String to = "keesun@whiteship.me";16String from = "whiteship@whiteship.me";17String host = "127.0.0.1";1819Properties properties = System.getProperties();20properties.setProperty("mail.smtp.host", host);2122Session session = Session.getDefaultInstance(properties);2324try {25MimeMessage message = new MimeMessage(session);26message.setFrom(new InternetAddress(from));27message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));28message.setSubject("Test Mail from Java Program");29message.setText("message");3031Transport.send(message);32} catch (MessagingException e) {33e.printStackTrace();34}35}36}
5.3 예시 - 변경
1// EmailMessage.java2package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;34public class EmailMessage {56private String from;78private String to;9private String cc;10private String bcc;1112private String subject;1314private String text;1516public String getFrom() {17return from;18}1920public void setFrom(String from) {21this.from = from;22}2324public String getTo() {25return to;26}2728public void setTo(String to) {29this.to = to;30}3132public String getSubject() {33return subject;34}3536public void setSubject(String subject) {37this.subject = subject;38}3940public String getText() {41return text;42}4344public void setText(String text) {45this.text = text;46}4748public String getCc() {49return cc;50}5152public void setCc(String cc) {53this.cc = cc;54}5556public String getBcc() {57return bcc;58}5960public void setBcc(String bcc) {61this.bcc = bcc;62}63}
1// EmailSettings.java2package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;34public class EmailSettings {56private String host;78public String getHost() {9return host;10}1112public void setHost(String host) {13this.host = host;14}15}
1// EmailSender.java2package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;34import java.util.Properties;5import javax.mail.Message;6import javax.mail.MessagingException;7import javax.mail.Session;8import javax.mail.Transport;9import javax.mail.internet.InternetAddress;10import javax.mail.internet.MimeMessage;1112public class EmailSender {1314private EmailSettings emailSettings;1516public EmailSender(EmailSettings emailSettings) {17this.emailSettings = emailSettings;18}1920/**21* 이메일 보내는 메소드22* @param emailMessage23*/24public void sendEmail(EmailMessage emailMessage) {25Properties properties = System.getProperties();26properties.setProperty("mail.smtp.host", emailSettings.getHost());2728Session session = Session.getDefaultInstance(properties);2930try {31MimeMessage message = new MimeMessage(session);32message.setFrom(new InternetAddress(emailMessage.getFrom()));33message.addRecipient(34Message.RecipientType.TO,35new InternetAddress(emailMessage.getTo())36);37message.addRecipient(38Message.RecipientType.CC,39new InternetAddress(emailMessage.getCc())40);41message.setSubject(emailMessage.getSubject());42message.setText(emailMessage.getText());4344Transport.send(message);45} catch (MessagingException e) {46e.printStackTrace();47}48}49}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._10_facade._02_after;34public class Client {56public static void main(String[] args) {7EmailSettings emailSettings = new EmailSettings();8emailSettings.setHost("127.0.0.1");910EmailSender emailSender = new EmailSender(emailSettings);1112EmailMessage emailMessage = new EmailMessage();13emailMessage.setFrom("keesun");14emailMessage.setTo("whiteship");15emailMessage.setCc("일남");16emailMessage.setSubject("오징어게임");17emailMessage.setText("밖은 더 지옥이더라고..");1819emailSender.sendEmail(emailMessage);20}21}
5.4 장단점
- 장점 : 서브 시스템에 대한 의존성을 한곳으로 모을 수 있다.
- 단점 : 퍼사드 클래스가 서브 시스템에 대한 모든 의존성을 가지게 된다
5.5 실무에서는 어떻게 쓰이나?
스프링
- Spring MVC
- 스프링이 제공하는 대부분의 기술 독립적인 인터페이스와 그 구현체
1package me.whiteship.designpatterns._02_structural_patterns._10_facade._03_java;23import org.springframework.jdbc.support.JdbcTransactionManager;4import org.springframework.mail.MailSender;5import org.springframework.mail.javamail.JavaMailSenderImpl;6import org.springframework.transaction.PlatformTransactionManager;78public class FacadeInSpring {910public static void main(String[] args) {11MailSender mailSender = new JavaMailSenderImpl();1213PlatformTransactionManager platformTransactionManager = new JdbcTransactionManager();14}15}
6. 플라이웨이트(Flyweight) 패턴
6.1 패턴 소개

객체를 가볍게 만들어 메모리 사용을 줄이는 패턴.
자주 변하는 속성(또는 외적인 속성, extrinsit)과변하지 않는 속성(또는 내적인 속성, intrinsit)을 분리하고,- 재사용하여 메모리 사용을 줄일 수 있다.
- 애플리케이션에서 굉장히 많은 인스턴스를 만드는 특징을 가지고 있는 애플리케이션에서 주로 사용한다.
- 많은 인스턴스를 만들다보면, 메모리 사용을 많이 하게 되서, 메모리가 부족해지는 현상이 발생한다.
- flyweight 패턴을 적용해, 공통되는 부분을 따로 모아서 재사용하는 패턴이다.
즉, 자주 변하는 속성(또는 외적인 속성, extrinsit)과 자주 변하지 않는 속성(또는 내적인 속성, intrinsit)을 분리해서 자주 변하지 않는 속성을 재사용하는 방법이다.
- cf. flyweight는 ‘가벼운‘이란 뜻으로, 복싱에서 체급을 나눌 떄, flywight라는 체급이 있다.

e.g. 문서에 수천 개의 글자가 있고, 모든 글자가 동일한 폰트 스타일을 사용한다면,
- 각 글자마다 폰트 스타일 정보를 별도로 저장하는 것은 메모리 낭비가 될 수 있다.
- 특정 폰트 스타일(예: 글꼴, 크기, 색상 등)과 같이 반복되는 정보를 공유 객체로 만들어 메모리 사용을 줄일 수 있다.
- 결과적으로 문서 내의 모든 텍스트가 동일한 폰트 스타일을 사용할 경우,
- 단 하나의 폰트 스타일 객체만 메모리에 존재하게 된다.
6.2 예시 - 기존
1// Character.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._01_before;34public class Character {56private char value;78private String color;910private String fontFamily;1112private int fontSize;1314public Character(char value, String color, String fontFamily, int fontSize) {15this.value = value;16this.color = color;17this.fontFamily = fontFamily;18this.fontSize = fontSize;19}20}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._01_before;34public class Client {56public static void main(String[] args) {7Character c1 = new Character('h', "white", "Nanum", 12);8Character c2 = new Character('e', "white", "Nanum", 12);9Character c3 = new Character('l', "white", "Nanum", 12);10Character c4 = new Character('l', "white", "Nanum", 12);11Character c5 = new Character('o', "white", "Nanum", 12);12}13}
6.3 예시 - 변경
1// Font.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;34public final class Font {56final String family;78final int size;910public Font(String family, int size) {11this.family = family;12this.size = size;13}1415public String getFamily() {16return family;17}1819public int getSize() {20return size;21}22}
1// FontFactory.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;34import java.util.HashMap;5import java.util.Map;67public class FontFactory {89private Map<String, Font> cache = new HashMap<>();1011public Font getFont(String font) {12if (cache.containsKey(font)) {13return cache.get(font);14} else {15String[] split = font.split(":");16Font newFont = new Font(split[0], Integer.parseInt(split[1]));17cache.put(font, newFont);18return newFont;19}20}21}
1// Character.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;34public class Character {56private char value;78private String color;910private Font font;1112public Character(char value, String color, Font font) {13this.value = value;14this.color = color;15this.font = font;16}17}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._02_after;34public class Client {56public static void main(String[] args) {7FontFactory fontFactory = new FontFactory();8Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));9Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));10Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));11}12}
6.4 장단점
- 장점 : 애플리케이션에서 사용하는 메모리를 줄일 수 있다.
- 단점 : 코드의 복잡도가 증가한다.
6.5 실무에서는 어떻게 쓰이나?
자바
- Integer.valueOf(int)
- 캐시를 제공한다.
- https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html#valueOf-int-
1// FlyweightInJava.java2package me.whiteship.designpatterns._02_structural_patterns._11_flyweight._03_java;34public class FlyweightInJava {56public static void main(String[] args) {7Integer i1 = Integer.valueOf(10);8Integer i2 = Integer.valueOf(10);9System.out.println(i1 == i2);10}11}
7. 프록시(Proxy) 패턴
7.1 패턴 소개

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

cf. 클라이언트가 게임 서비스를 사용해서 게임을 시작한다.
- 게임 클라이언트가 게임 서버에 직접 접근하는 대신, Proxy 서버를 통해 접근한다.
- 이 Proxy 서버는 클라이언트의 요청을 검증하고, 적절한 경우에만 게임 서버로 요청을 전달한다.
7.2 예시 - 기존
1// GameService.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._01_before;34public class GameService {56public void startGame() {7System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");8}9}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._01_before;34public class Client {56public static void main(String[] args) throws InterruptedException {7GameService gameService = new GameService();8gameService.startGame();9}10}
7.3 예시 - 변경(상속 사용)
기존을 코드를 변경하지 않고 적용하는 방법
1// GameService.java2public class GameService {34public void startGame() throws InterruptedException {5System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");6Thread.sleep(1000L);7}89}
1// GameServiceProxy.java2public class GameServiceProxy extends GameService {34@Override5public void startGame() throws InterruptedException {6long before = System.currentTimeMillis();7super.startGame();8System.out.println(System.currentTimeMillis() - before);9}10}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;34public class Client {56public static void main(String[] args) {7GameService gameService = new GameServiceProxy();8gameService.startGame();9}10}
7.4 예시 - 변경(인터페이스 사용)
1// GameService.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;34public interface GameService {5void startGame();6}
1// DefaultGameService.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;34public class DefaultGameService implements GameService {56@Override7public void startGame() {8System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");9}10}
1// GameServiceProxy.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;34public class GameServiceProxy implements GameService {56private GameService gameService;78@Override9public void startGame() {10long before = System.currentTimeMillis();11if (this.gameService == null) {12this.gameService = new DefaultGameService();13}1415gameService.startGame();16System.out.println(System.currentTimeMillis() - before);17}18}
1// Client.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after;34public class Client {56public static void main(String[] args) {7GameService gameService = new GameServiceProxy();8gameService.startGame();9}10}
7.5 장단점
- 장점
- 기존 코드를 변경하지 않고, 새로운 기능을 추가할 수 있다.
- 기존 코드가 해야 하는 일만 유지할 수 있다.
- 기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다.
- 단점 : 코드의 복잡도가 증가한다
7.6 실무에서 어떻게 쓰이나?
특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴.
- 자바 : 다이나믹 프록시, java.lang.reflect.Proxy
1// ProxyInJava.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;34import java.lang.reflect.Proxy;5import me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after.DefaultGameService;6import me.whiteship.designpatterns._02_structural_patterns._12_proxy._02_after.GameService;78public class ProxyInJava {910public static void main(String[] args) {11ProxyInJava proxyInJava = new ProxyInJava();12proxyInJava.dynamicProxy();13}1415private void dynamicProxy() {16GameService gameServiceProxy = getGameServiceProxy(17new DefaultGameService()18);19gameServiceProxy.startGame();20}2122private GameService getGameServiceProxy(GameService target) {23return (GameService) Proxy.newProxyInstance(24this.getClass().getClassLoader(),25new Class[] { GameService.class },26(proxy, method, args) -> {27System.out.println("O");28method.invoke(target, args);29System.out.println("ㅁ");30return null;31}32);33}34}
- 스프링 : 스프링 AOP
1// PerfAspect.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;34import org.aspectj.lang.ProceedingJoinPoint;5import org.aspectj.lang.annotation.Around;6import org.aspectj.lang.annotation.Aspect;7import org.springframework.stereotype.Component;89@Aspect10@Component11public class PerfAspect {1213@Around("bean(gameService)")14public void timestamp(ProceedingJoinPoint point) throws Throwable {15long before = System.currentTimeMillis();16point.proceed();17System.out.println(System.currentTimeMillis() - before);18}19}
1// GameService.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;34import org.springframework.cache.annotation.Cacheable;5import org.springframework.stereotype.Service;6import org.springframework.transaction.annotation.Transactional;78@Service9public class GameService {1011public void startGame() {12System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");13}14}
1// App.java2package me.whiteship.designpatterns._02_structural_patterns._12_proxy._03_java;34import org.springframework.boot.ApplicationRunner;5import org.springframework.boot.SpringApplication;6import org.springframework.boot.WebApplicationType;7import org.springframework.boot.autoconfigure.SpringBootApplication;8import org.springframework.context.annotation.Bean;910@SpringBootApplication11public class App {1213public static void main(String[] args) {14SpringApplication app = new SpringApplication(App.class);15app.setWebApplicationType(WebApplicationType.NONE);16app.run(args);17}1819@Bean20public ApplicationRunner applicationRunner(GameService gameService) {21return args -> {22gameService.startGame();23};24}25}