본문 바로가기
3학년 2학기 학사/소프트웨어 분석 및 설계

[소프트웨어 분석 및 설계] L18. Composite Pattern, Decorator Pattern

by whiteTommy 2024. 12. 13.
반응형

Composite Pattern

  • Composite Pattern (복합체 패턴) 이란?
    • 전체-부분 관계의 트리 구조로 표현되는 객체들을 단일 객체처럼 취급할 수 있게 해주는 구조 패턴
      • 단일 객체와 복합 객체를 동일한 인터페이스를 사용하여 처리하기 위함
        • 단일 객체는 Leaf 라고 하고, 단일 객체들의 그룹인 복합 객체는 Composite 이라고 함
  • 복합체 패턴이 필요한 상황 예시
    • 주문 시스템에서 여러 상품을 담은 상자의 가격을 계산하려고 할 때
      • 한 box 에 여러 products 와 좀 더 작은 box 들을 담을 수 있음
      • 작은 box 들 역시 여러 products 를 담을 수 있다고 있음
    • 상자의 가격을 계산하기 위해서는 내부 제품을 모두 살펴보면서 가격을 합산해야 함
      • 트리 전체 순회를 하면서 box 와 product 의 type 을 구분해주는 코드를 구성해야 해서 코드가 복잡해질 수 있음
  • 복합체 패턴의 아이디어
    • 총가격을 계산하는 메소드를 가지는 공통 인터페이스를 통해서 products 와 boxes 에 대해 동일하게 작업하게 할 수 있음
      • Product의 경우 단순히 제품의 가격을 반환
      • Box 의 경우 Box 내 products 의 총 가격을 반환
    • 트리를 구성하는 객체들의 구체적인 클래스 타입에 대해서 신경 쓸 필요가 없음 (현재 보고 있는 객체가 product 인지 box 인지 알 필요가 없음)
    • 복합체 패턴은 그릇과 내용물을 동일 시 해서 재귀적인 구조를 편하게 다룰 수 있게 해줌
  • 복합체 패턴의 구조
  • 복합체 패턴의 구조 예시 - Box & Product
  • 복합체 패턴 코드 예시
    interface Component{
        int getPrice();
        String getName();
    }
    
    public class Product implements Component{
        String name;
        int price;
        
        public Product(String name, int price){
        	this.name = name;
            this.price = price;
        }
        
        @Override
        public int getPrice(){
        	return this.price
        }
        
        @Override
        public String getName(){
        	return this.name;
        }
    }
    
    public class Box implements Component{
        List<Component> components;
        String name;
        
        public Box(String name){
        	components = new ArrayList<>();
            this.name = name;
        }
        
        public void add(Component item){
        	components.add(item);
        }
        
        public List<Component> getComponents(){
        	return components;
        }
        
        @Override 
        public int getPrice(){
        	int totalprice = 0;
            for(Component component : components)
            	totalPrice += component.getPrice();
            return totalPrice;
        }
        
        @Override
        public String getName(){
        	return this.name;
        }
    }
    
    public class Client{
        public void printPrint(Component box){
        	int totalPrice = box.getPrice();
            String line = String.format("Total price of %s: %d", box.getName(), box.getPrice());
            System.out.println(line);
        }
    }
    
    public class Main{
        public static void main(String[] args){
        	Box box = new Box("Main box");
            Product armor = new Product("Armor", 250);
            Product sword = new Product("Sword", 500);
            
            box.add(armor);
            box.add(sword);
            
            Box sub_box = new Box("Sub box");
            Product apple = new Product("Apple", 400);
            Product banana = new Product("banana", 130);
    
            sub_box.add(apple);
            sub_box.add(banana);
            
            box.add(sub_box);
            
            Client client = new Client();
            client.printPrint(box);
            
        }
    }
  • Java 에서 복합체 패턴 사용 사례
    • Java Swing
      JFrame frame = new JFrame();
      
      JTextField textField = new JTextField();
      textField.setBounds(200,200,200,40);
      frame.add(textField);
      
      JButton button = new JButton("click");
      button.setBounds(200,100,60,40);
      button.addActionListener(e->textField.setText("Hello Swing"));
      frame.add(button);
      
      frame.setSize(600,400);
      frame.setLayout(null);
      frame.setVisible(true);
  • 사용 시기
    • 객체의 구조가 트리로 표현되는 상황에서, 단일/복합 객체의 관계를 단순화하여 균일하게 처리하고 싶을
  • 장점
    • 다형성 재귀를 통해 복잡한 트리 구조를 보다 편리하게 다룰 수 있음
    • OCP 준수
      • 새로운 leaf 클래스 추가하더라도 클라이언트에 영향 없음 
  • 단점
    • 재귀 호출 특징 상 트리 깊이가 깊어지면 디버깅에 어려움이 생김
    • 기능이 너무 다른 클래스들 간에는 공통 인터페이스 설계가 까다로움
      • Component 에 선언되는 메소드가 공통으로 활용될 수 있는 의미를 가져야 함

 

 

Decorator Pattern

  • 데코레이터 패턴이란?
    • 객체에 추가적인 기능을 동적으로 더해줄 수 있게 해주는 구조 패턴
      • 컴파일 타임이 아닌 런타임에 객체에 대한 기능 확장이 가능
    • 데코레이터(장식자) 라는 뜻은 기본 제품에 재료/디자인을 추가 및 변경해줌으로써 새로운 종류의 제품을 만들어내는 것을 의미 
  • 데코레이터 패턴이 필요한 상황
    • 사용자에게 이벤트 알람을 주기 위한 알람 라이브러리를 설계
    • Application 에서는 Notifier 를 통해 기본적으로 이메일로 알람을 함
    • 어느 시점부터 사용자들이 이메일 이상의 알람 기능을 원하여 아래와 같이 상속 구조로 Notifier 를 확장했다고 가정
      • 상속 구조에서는 한번에 특정 Notifier 만 동작하게 됨
      • 여러 채널을 통해서 알람을 보내고 싶으면?
    • 상속 구조를 유지한 채로는 모든 조합에 따른 Notifier 를 구현해야 함
      • 코드의 양도 늘어나고 확장의 유연성도 떨어짐
      • 이미 있는 여러 Notifier 들을 runtime 에 조립할 수 있을까? => 데코레이터 패턴
  • 데코레이터 패턴의 구조
  • 데코레이터 패턴 구조 예시 
  • 데코레이터 패턴 코드 예시
    • SMSDecorator, SlackDecorator 도 아래와 유사하게 구현
      • send 메소드를 각각에 맞게 오버라이딩 해주되, super 의 send 를 호출해주어야 함
        interface Notifier {
            void send(String message);
        }
        
        public class DefaultNotifier implements Notifier {
            public void send(String message) {
                System.out.println("Email: " + message);
            }
        }
        
        public class BaseDecorator implements Notifier {
            private Notifier notifier;
        
            public BaseDecorator(Notifier wrapper) {
                this.notifier = wrapper;
            }
        
            @Override
            public void send(String message) {
                this.notifier.send(message); // 기존 기능 유지
            }
        }
        
        public class FacebookDecorator extends BaseDecorator {
            public FacebookDecorator(Notifier wrapper) {
                super(wrapper);
            }
        
            @Override
            public void send(String message) {
                super.send(message); // 기존 기능 호출
                System.out.println("[Facebook] " + message); // 추가 기능
            }
        }
        
        public class Main {
            public static void main(String[] args) {
                boolean facebookEnabled = false;
                boolean smsEnabled = true;
                boolean slackEnabled = false;
        
                Notifier notifier = new DefaultNotifier(); // 기본 알림 (이메일)
        
                if (facebookEnabled) {
                    notifier = new FacebookDecorator(notifier);
                }
                if (smsEnabled) {
                    notifier = new SMSDecorator(notifier);
                }
                if (slackEnabled) {
                    notifier = new SlackDecorator(notifier);
                }
        
                Application app = new Application(notifier);
                app.sendMessage("Hello World");
            }
        }


  • Java 에서 데코레이터 패턴 사용 사례
    • Stream
      • 어댑터이면서 동시에 데코레이터라고 볼 수 있음
    • Collections
      • checkedList(), synchronizedList(), unmodifiableList() 등
  • 사용 시기
    • 객체 책임과 행동이 동적으로 상황/조건에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
      • 객체를 생성하는 코드에 변경이 없으면서 런타임에 추가 가능
    • 객체의 결합을 통해 기능이 생성이 되어야 하는 경우
    • 상속을 통해 서브 클래싱으로 객체의 동작을 확장하는 것이 어색하거나 불가능할 때
  • 장점
    • SRP 준수
      • 각 데코레이터 클래스 마다 고유의 책임을 가짐
    • OCP 준수
      • 클라이언트 코드 수정 없이 기능 확장이 필요하면 데코레이터 클래스를 추가하면 됨
    • DIP 준수
      • 구현체가 아닌 인터페이스를 바라보기 때문
    • 데코레이터를 사용하면 상속을 통해 서브클래스로 확장하는 것보다 훨씬 유연하게 기능을 확장할 수 있음
    • 객체를 여러 데코레이터로 래핑하여 여러 동작을 클라이언트가 자유롭게 결합할 수 있음
  • 단점
    • 만일 장식자 일부를 동적으로 제거하고 싶다면, wrapper 스택에서 특정 wrapper 를 제거하는 것이 어려움
    • 데코레이터를 조합하는 방식에 코드 복잡도가 올라갈 수 있음
    • 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정되는데, 만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어려움
      • 어느 데코레이터를 먼저 감싸느냐에 따라 그에 대한 행동이 다르기에 순서에 유의

 

 

 

 

 

 

반응형