디자인패턴 - 구조 패턴

김상운(개발둥이)·2022년 10월 18일
1

디자인패턴

목록 보기
2/3
post-thumbnail

구조 패턴의 종류

구조 패턴이란: 구조패턴(structural patterns)은 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴이다.
예를 들어 서로 다른 인터페이스를 지닌 2개의 객체를 묶어 단일 인터페이스를 제공하거나 객체들을 서로 묶어 새로운 기능을 제공하는 패턴이다. 이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 것처럼 사용할 수 있다.

또, 여러 인터페이스를 합성하여 서로 다른 인터페이스들의 통일된 추상을 제공한다. 가장 중요한 점은 인터페이스나 구현을 복합하는 것이 아니라 객체를 합성하는 방법을 제공한다는 것이다.

  1. 어댑터 패턴
  2. 브릿지 패턴
  3. 컴포짓 패턴
  4. 데코레이터 패턴
  5. 퍼사드 패턴
  6. 플라이웨이트 패턴
  7. 프록시 패턴

어댑터 패턴

설명

  • 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴
  • 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.

구현

client 코드

public class LoginHandler {

    UserDetailsService userDetailsService;

    public LoginHandler(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public String login(String username, String password) {
        UserDetails userDetails = userDetailsService.loadUser(username);
        if (userDetails.getPassword().equals(password)) {
            return userDetails.getUsername();
        } else {
            throw new IllegalArgumentException();
        }
    }
}

클라이언트 코드는 UserDetailsService, UserDetails 타입으로 로직을 수행하기 때문에 AccountService, Account 타입의 객체를 클라이언트 코드에서 쓸 수 있게 타입을 UserDetailsService, UserDetails 로 바꿔줘야 한다.

adaptee

Account

public class Account {

    private String name;

    private String password;

    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}

AccountService

public class AccountService {

    public Account findAccountByUsername(String username) {
        Account account = new Account();
        account.setName(username);
        account.setPassword(username);
        account.setEmail(username);
        return account;
    }

    public void createNewAccount(Account account) {

    }

    public void updateAccount(Account account) {

    }

}

adaptee 객체를 adaptor 를 통해서 클라이언트 코드에서 원하는 타입으로 바꿔보자!

adaptor

AccountUserDetails

public class AccountUserDetails implements UserDetails {

    private Account account;

    public AccountUserDetails(Account account) {
        this.account = account;
    }

    @Override
    public String getUsername() {
        return account.getName();
    }

    @Override
    public String getPassword() {
        return account.getPassword();
    }
}

AccountUserDetailsService

public class AccountUserDetailsService implements UserDetailsService {

    private AccountService accountService;

    public AccountUserDetailsService(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public UserDetails loadUser(String username) {
        return new AccountUserDetails(accountService.findAccountByUsername(username));
    }
}

사용

public class App {

    public static void main(String[] args) {
        AccountService accountService = new AccountService();
        UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
        
        LoginHandler loginHandler = new LoginHandler(userDetailsService);
        String login = loginHandler.login("keesun", "keesun");
        
        System.out.println(login);
    }
}

어댑터 패턴을 이용하여 한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환하였다.

장단점

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

브릿지 패턴

  • 추상적인 것과 구체적인 것을 분리하여 연결하는 패턴
  • 하나의 계층 구조일 때 보다 각기 나누었을 때 독립적인 계층 구조로 발전 시킬 수 있다.
  • 구현부에서 추상층을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하도록 한다.
  • 즉 기능과 구현에 대해서 두 개를 별도의 클래스로 구현을 한다.
  • 기능추가와 구현추가를 분리한 패턴이다.

구현

리그오브레전드의 챔피언(구현)과 스킨(기능)을 통해 설명하겠다.

챔피언 마다 착용하는 스킨이 여러가지인데 상속을 사용하면 KDA아리, KDA카이사, 풀파티 아리, 풀파티 카이사 등 챔피언과 스킨이 한 클래스에 섞여 있다.(SRP 위반)

챔피언이 100개, 스킨이 100개 일 경우 100 X 100 의 클래스를 정의하여야 한다.

이를 해결하기 위해 구현부에 해당하는 챔피언에서 추상적인 스킨을 분리한다.

구현부

champion 인터페이스

public interface Champion  {

    void name();

    void move();

    void skillQ();

    void skillW();

    void skillE();

    void skillR();

}

DefaultChampion - 추상 클래스


public class DefaultChampion implements Champion {

    private Skin skin;
    private String name;

    public DefaultChampion(Skin skin, String name) {
        this.skin = skin;
        this.name = name;
    }

    @Override
    public void name() {
        System.out.println(skin.getName() + " " + this.name);
    }

    @Override
    public void move() {
        System.out.printf("%s %s move\n", skin.getName(), this.name);
    }

    @Override
    public void skillQ() {
        System.out.printf("%s %s Q\n", skin.getName(), this.name);
    }

    @Override
    public void skillW() {
        System.out.printf("%s %s W\n", skin.getName(), this.name);
    }

    @Override
    public void skillE() {
        System.out.printf("%s %s E\n", skin.getName(), this.name);
    }

    @Override
    public void skillR() {
        System.out.printf("%s %s R\n", skin.getName(), this.name);
    }

}

아칼리 챔피언

public class 아칼리 extends DefaultChampion {

    public 아칼리(Skin skin) {
        super(skin, "아칼리");
    }
}

아리 챔피언

public class 아리 extends DefaultChampion {

    public 아리(Skin skin) {
        super(skin, "아리");
    }
}

추상층

Skin - 인터페이스

public interface Skin {
    String getName();
}

KDA 스킨

public class KDA implements Skin {

    @Override
    public String getName() {
        return "KDA";
    }
}

풀파티 스킨

public class PoolParty implements Skin {

    @Override
    public String getName() {
        return "풀파티";
    }
}

사용

public class App {

    public static void main(String[] args) {
        //기능 추가
        Skin poolParty = new PoolParty();
        Skin KDA = new KDA();
    
        //구현 추가
        Champion ahri = new 아리(poolParty);
        Champion akali = new 아칼리(KDA);

        ahri.name();
        ahri.move();
        ahri.skillQ();
        ahri.skillW();

        akali.name();
    }
}

구현부에 해당하는 챔피언에서 추상층에 해당하는 스킨을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하게 하였다.

장단점

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

컴포짓 패턴

  • 컴포지트 패턴(Composite pattern)이란 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한다.
  • 그룹 전체와 개별 객체를 동일하게 처리할 수 있는 패턴.
  • 클라이언트 입장에서는 '전체'나 '부분'이나 모두 동일한 컴포넌트로 인식할 수는 계층 구조를 만든다. (Part-Whole Hierarchy)

구현

클라이언트 코드

public class Client {

    public static void main(String[] args) {
        Item doranBlade = new Item("도란검", 450);
        Item healPotion = new Item("체력 물약", 50);

        Bag bag = new Bag();

        bag.add(doranBlade);
        bag.add(healPotion);

        Client client = new Client();

        client.printPrice(doranBlade);
        client.printPrice(bag);
    }

    private void printPrice(Component item) {
        System.out.println(item.getPrice());
    }
    
}
  • Item 은 단일 객체이고, Bag 은 Item 을 담을 수 있는 복합 객체이다.
  • printPrice 에서 개별 객체인 Item 이든, 복합 객체인 Bag 이든 동일하게 처리한다.
  • 동일하게 처리를 위해 Component 인터페이스를 정의하여 타입 추상화를 한다.

Component

public interface Component {

    int getPrice();

}

클라이언트 코드에서 getPrice() 를 필요로 하기 때문에 Item 객체와 Bag 객체 둘다 getPrice() 를 오버라이딩 한다.

단일 객체

public class Item implements Component {

    private String name;

    private int price;

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

복합 객체

public class Bag implements Component {

    private List<Component> components = new ArrayList<>();

    public void add(Component item) {
        components.add(item);
    }

    public List<Component> getComponents() {
        return components;
    }

    @Override
    public int getPrice() {
        return components.stream().mapToInt(Component::getPrice).sum();
    }
}

Bag 은 Component 객체를 내부에 저장할 수 있는 복합 객체이기 때문에 stream 을 활용하여 전체를 더한 값을 반환한다.

장단점

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

데코레이터 패턴

  • 데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.
  • 기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴
  • 상속이 아닌 위임을 사용해서 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하

구현

클라이언트 코드

public class Client {

    private CommentService commentService;

    public Client(CommentService commentService) {
        this.commentService = commentService;
    }

    public void writeComment(String comment) {
        commentService.addComment(comment);
    }

    public static void main(String[] args) {
        //기본적인 동작을 수행
        CommentService commentService = new DefaultCommentService();

        commentService = new SpamFilteringCommentDecorator(new TrimmingCommentDecorator(commentService));

        Client client = new Client(commentService);
        client.writeComment("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }

}
  • DefaultCommentService 타입의 객체를 SpamFilteringCommentDecorator, TrimmingCommentDecorator 객체가 꾸며준다.
  • 상속을 사용해서 spamFilter, trimming 기능을 사용한 것이 아닌 합성을 사용한다.

component


public interface CommentService {

    void addComment(String comment);

}

concreteComponent

public class DefaultCommentService implements CommentService {
    @Override
    public void addComment(String comment) {
        System.out.println(comment);
    }
}

기본동작을 하는 객체.

decorator

SpamFilteringCommentDecorator

public class SpamFilteringCommentDecorator extends CommentDecorator {

    public SpamFilteringCommentDecorator(CommentService commentService) {
        super(commentService);
    }

    @Override
    public void addComment(String comment) {
        if (isNotSpam(comment)) {
            super.addComment(comment);
        }
    }

    private boolean isNotSpam(String comment) {
        return !comment.contains("http");
    }
}

TrimmingCommentDecorator

public class TrimmingCommentDecorator extends CommentDecorator {

    public TrimmingCommentDecorator(CommentService commentService) {
        super(commentService);
    }

    @Override
    public void addComment(String comment) {
        super.addComment(trim(comment));
    }

    private String trim(String comment) {
        return comment.replace("...", "");
    }
}

기본 동작을 꾸며주는 decorator 객체

장단점

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

퍼사드 패턴

  • 복잡한 서브 시스템 의존성을 최소화하는 방법.
  • 클라이언트가 사용해야 하는 복잡한 서브 시스템 의존성을 간단한 인터페이스로 추상화 할
    수 있다.
  • 퍼사드는 라이브러리 바깥쪽의 코드가 라이브러리의 안쪽 코드에 의존하는 일을 감소시켜준다.
  • 대부분의 바깥쪽의 코드가 퍼사드를 이용하기 때문에 시스템을 개발하는 데 있어 유연성이 향상된다.

구현

퍼사드 패턴 적용 전

public class Client {

    public static void main(String[] args) {
        String to = "keesun@whiteship.me";
        String from = "whiteship@whiteship.me";
        String host = "127.0.0.1";

        Properties properties = System.getProperties();
        properties.setProperty("mail.smtp.host", host);

        Session session = Session.getDefaultInstance(properties);

        try {
            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(from));
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
            message.setSubject("Test Mail from Java Program");
            message.setText("message");

            Transport.send(message);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }
}
  • 메일을 보내는 로직이다.
  • 메일을 보내기 위해 라이브러리등에 대한 의존성이 강한 코드 뭉치들이다.
  • 퍼사드 패턴을 적용하여 라이브러리를 이용하는데 있어 유연성을 향상할 필요가 있다.

적용 후

EmailMessage

public class EmailMessage {

    private String from;

    private String to;
    private String cc;
    private String bcc;

    private String subject;

    private String text;

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getCc() {
        return cc;
    }

    public void setCc(String cc) {
        this.cc = cc;
    }

    public String getBcc() {
        return bcc;
    }

    public void setBcc(String bcc) {
        this.bcc = bcc;
    }
}

이메일에 보낼 메시지를 저장할 객체

EmailSettings

public class EmailSettings {

    private String host;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }
}

이메일을 보내기 위한 세팅정보를 저장할 객체

EmailSender

public class EmailSender {

    private EmailSettings emailSettings;

    public EmailSender(EmailSettings emailSettings) {
        this.emailSettings = emailSettings;
    }

    /**
     * 이메일 보내는 메소드
     * @param emailMessage
     */
    public void sendEmail(EmailMessage emailMessage) {
        Properties properties = System.getProperties();
        properties.setProperty("mail.smtp.host", emailSettings.getHost());

        Session session = Session.getDefaultInstance(properties);

        try {
            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(emailMessage.getFrom()));
            message.addRecipient(Message.RecipientType.TO, new InternetAddress(emailMessage.getTo()));
            message.addRecipient(Message.RecipientType.CC, new InternetAddress(emailMessage.getCc()));
            message.setSubject(emailMessage.getSubject());
            message.setText(emailMessage.getText());

            Transport.send(message);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }
}

이메일을 보내기 위한 객체

적용 후 클라이언트 코드

public class Client {

    public static void main(String[] args) {
        EmailSettings emailSettings = new EmailSettings();
        emailSettings.setHost("127.0.0.1");

        EmailSender emailSender = new EmailSender(emailSettings);

        EmailMessage emailMessage = new EmailMessage();
        emailMessage.setFrom("keesun");
        emailMessage.setTo("whiteship");
        emailMessage.setCc("일남");
        emailMessage.setSubject("오징어게임");
        emailMessage.setText("밖은 더 지옥이더라고..");

        emailSender.sendEmail(emailMessage);
    }
}

퍼사드 패턴 적용 후 코드가 더 깔끔해진 것을 확인할 수 있다.

장단점

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

플라이웨이트 패턴

  • 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴.
  • 자주 변하는 속성(또는 외적인 속성, extrinsit)과 변하지 않는 속성(또는 내적인 속성,
    intrinsit)을 분리하고 재사용하여 메모리 사용을 줄일 수 있다.

구현

  • 글자(Character) 객체는 글자, 글자 색, 폰트, 글자 크기를 속성으로 가지고 있다.
  • 글자 객체를 생성하는데 있어 자주 사용되는 속성이 있다면 재사용하여 메모리를 아낄 수 있다.

예시

        Character c1 = new Character('h', "white", "Nanum", 12);
        Character c2 = new Character('e', "white", "Nanum", 12);
        Character c3 = new Character('l', "white", "Nanum", 12);
        Character c4 = new Character('l', "white", "Nanum", 12);
        Character c5 = new Character('o', "white", "Nanum", 12);

위의 코드처럼 Character 객체를 만들 경우 글자 색, 폰트, 글자 크기 반복하여 사용하는 것을 알 수 있다.

플라이웨이트 패턴을 적용하여 메모리를 아껴보자!

적용 대상

public class Character {

    private char value;

    private String color;

    private String fontFamily;

    private int fontSize;

    public Character(char value, String color, String fontFamily, int fontSize) {
        this.value = value;
        this.color = color;
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
    }
}

플라이웨이트

public final class Font {

    final String family;

    final int size;

    public Font(String family, int size) {
        this.family = family;
        this.size = size;
    }

    public String getFamily() {
        return family;
    }

    public int getSize() {
        return size;
    }
}

Character 객체의 폰트와, 글자 크기를 저장할 객체이다. 불변 객체이기 때문에 상속을 막기 위해 클래스와 필드를 final로 하였다.

플라이웨이트 팩토리

public class FontFactory {

    private Map<String, Font> cache = new HashMap<>();

    public Font getFont(String font) {
        if (cache.containsKey(font)) {
            return cache.get(font);
        } else {
            String[] split = font.split(":");
            Font newFont = new Font(split[0], Integer.parseInt(split[1]));
            cache.put(font, newFont);
            return newFont;
        }
    }
}

들어오는 값을 split 하여 font 객체를 만든다. 팩토리 내부에 map 을 두어 값이 있을 경우 map에 저장되어 있는 값을 반환하고, 없을 경우 새로 만들어 map 에 저장하여 반환한다.

장단점

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

프록시 패턴

  • 특정 객체에 대한 접근을 제어하거나 기능을 추가할 수 있는 패턴.
  • 초기화 지연, 접근 제어, 로깅, 캐싱 등 다양하게 응용해 사용 할 수 있다.

구현

프록시 객체든, 타겟 객체든 둘다 같은 타입으로 추상화하여야 사용하는 클라이언트 코드에서 내부 구현을 모른체 사용이 가능하다.

subject

public interface GameService {

    void startGame();

}

타입추상화를 위해 정의하며, 클라이언트 코드에서 사용할 메서드를 정의한다.

proxy

public class GameServiceProxy implements GameService {

    private GameService gameService;

    @Override
    public void startGame() {
        long before = System.currentTimeMillis();
        if (this.gameService == null) {
            this.gameService = new DefaultGameService();
        }

        gameService.startGame();
        System.out.println(System.currentTimeMillis() - before);
    }
}

프록시 객체로 대리자 역할을 하는데, target 객체를 호출하기 전이나 후로 추가적인 기능이 가능하며, 위 코드는 지연로딩을 활용하였다.

target

public class DefaultGameService implements GameService {

    @Override
    public void startGame() {
        System.out.println("이 자리에 오신 여러분을 진심으로 환영합니다.");
    }
}

실제 클라이언트 코드에 기능을 제공할 겍체이다.

클라이언트 코드

public class Client {

    public static void main(String[] args) {
        GameService gameService = new GameServiceProxy();
        gameService.startGame();
    }
}

클라이언트 코드는 proxy 객체든 real 객체든 타입이 같기 때문에 좀 더 유연한 학장이 가능하다.

장단점

  • 장점
    • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
    • 기존 코드가 해야 하는 일만 유지할 수 있다.
    • 기능 추가 및 초기화 지연 등으로 다양하게 활용할 수 있다.
  • 단점
    • 코드의 복잡도가 증가한다.
profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글