구조 관련 : Composite 패턴

    들어가기 전

    이 글은 인프런 백기선님의 GOF 강의를 복습하며 작성한 글입니다.

     


    Composite 패턴

    • GOF : 클라이언트가 그룹 전체 / 개별 객체를 동일하게 처리할 수 있는 패턴 
      • 클라이언트 입장에서는 어떤 객체가 전체 / 부분인지 모름 (계층 트리의 Root, Leap Node인지 구별 X)
      • Composite 패턴은 'Tree 구조'를 구성해야 한다는 제약 사항이 있음. 
    • 디자인 Component
      • Component : 트리 구조를 추상화 함. 
      • Composite : 그룹을 표현할 수 있는 객체임.  (Root Node의 개념)
        1. 어떤 액션을 공통으로 제공할지 고려해야함. 
        2. 내부적으로 Component 배열을 가짐. (타입은 Leaf가 아니라 반드시 Component임) 
    • 클라이언트는 Component 인터페이스에만 의존함. (Component 하위에 Composite, Leaf가 있음) 
    • Component 하위 클래스로 Composite, Leaf를 구성해야 함.
      • Item, Bag 같은 것들이 Composite, Leaf 중에 하나가 될 수 있음. 

     


    디자인 패턴이 필요한 코드 

    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(Item item) {
            System.out.println(item.getPrice());
        }
    
        private void printPrice(Bag bag) {
            int sum = bag.getItems().stream().mapToInt(Item::getPrice).sum();
            System.out.println(sum);
        }
    
    }

    이 클라이언트 코드는 Composite 패턴을 이용하면 코드를 개선할 수 있는 형태다. 

    • Item은 가격이 얼마인지 제공해주고, Bag에는 여러 Item을 넣을 수 있음. 
    • 클라이언트는 Bag에 있는 Item 전체의 값이 궁금할 수도 있음. 

    이 경우, printPrice(Item item), printPrice(Bag bag) 같은 메서드들을 계속 추가해야 할 것이다. 즉, Item이 추가될 때 마다 기존 코드에 변경점이 발생할 것이다. 

    한 가지 더 고려해야할 부분은 printPrice(Bag bag) 메서드가 클라이언트에 있는 것이 맞는지다. 왜냐하면 클라이언트가 이 부분을 처리하기에는 고려해야 할 부분이 너무 많기 때문이다.

    • Bag 안에 Bag이 있는 경우가 있을 수도 있음.  (클라이언트가 이렇게 자세한 경우도 고려해야 할까?)
    • Bag 안에 있는 전체 Item의 값을 왜 클라이언트가 구해야 하는 것일까? (대부분의 필드는 Bag 안에 있음) 

    이런 두 가지 문제들은 Bag의 캡슐화가 깨져서 클라이언트가 많은 부분을 알아야 하기 때문에 발생한다. Composite 패턴을 이용해서 캡슐화해서 대응해 볼 수 있다. Bag, Item에 대한 계층 구조로 만들고, 클라이언트는 최상위 인터페이스에만 의존해서 이런 문제들에서 해방되는 것이다. 

     


    디자인 패턴 적용하기 

    Composite 디자인 패턴을 적용하기 위해서는 Component 인터페이스, Composite, Leaf 노들을 먼저 정의해야한다. 

    • Component 인터페이스 : getPrice()만 제공한다. 
    • Composite : Bag 클래스가 된다. Component를 구현한다. Composite는 내부에 Component []을 가진다. (절대로 리프노드 타입이 아님) 
    • Leaf : Item 클래스가 된다. Component를 구현한다. 

    Component 인터페이스는 getPrice() 메서드만 제공을 하는데, 이것은 이전 코드에서 Bag + Item의 가격을 구하는데 공통적으로 getPrice() 메서드를 이용했기 때문에 추상화하는 것이다. 

    private void printPrice(Component component) {
        System.out.println(component.getPrice());
    }

    클라이언트는 이전에는 구체적인 타입 (Item, Bag)에 의존했으나 여기서부터는 최상위 인터페이스인 Component에만 의존하도록 수정한다. 

    // 최상위 인터페이스 : Component
    public interface Component {
        int getPrice();
    }
    
    // Composite으로 동작
    public class Bag implements Component {
        private final List<Component> items = new ArrayList<>();
        public void add(Component item) { this.items.add(item); }
        @Override
        public int getPrice() {
            return this.items.stream()
                    .mapToInt(Component::getPrice)
                    .sum();
        }
    }
    
    // Leaf 노드 구현
    public class Item implements Component{
        private final String name;
        private final int price;
    
        public Item(String name, int price) {
            this.name = name;
            this.price = price;
        }
    
        @Override public int getPrice() { return this.price; }
    }

    다음 코드로 Component 인터페이스를 최상위로 가지는 트리 구조를 만들 수 있다. 

    • Composite을 담당하는 Bag은 내부적으로 Component 배열을 가지고 있다. (Root 노드의 개념) 
    • Item, Bag은 동일하게 Component 인터페이스를 구현함.

    기존에는 Bag에 있는 Item의 총합을 구할 때, 클라이언트가 직접 계산을 했어야했고 이 부분은 캡슐화가 깨진 것이다. 그러나 이렇게 코드를 구현해두면 Bag이 가진 Item 가격의 합을 구하는 로직이 클라이언트로부터 캡슐화된다. 즉, 클라이언트는 지나치게 많은 것들을 알지 못해도 괜찮아진다. 

    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 component) {
            System.out.println(component.getPrice());
        }
    
    }

    클라이언트는 이전과 다르게 printPrice(Component component) 메서드만 사용하는데, Bag + Item이 공통된 인터페이스인 Component를 통해서 getPrice()를 제공한다. 그리고 이 getPrice()에서 각 Component의 가격을 구하는 것이 캡슐화 되었기 때문에 클라이언트는 Component 객체에게 위임하기만 하면 된다. 

     


    Composite 패턴의 장/단점

    • 장점
      • 복잡한 트리 구조를 편리하게 사용할 수 있음. (클라이언트는 트리의 Head / Leaf인지 구별하지 않아도 됨)
      • 다형성 +  재귀를 활용하기 좋음. 
      • 클라이언트 코드를 변경하지 않고 새로운 Element(Leaf Node / Composite)를 추가할 수 있음. 
    • 단점
      • 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야하기 때문에, 예를 들면 getPrice()) 지나치게 억지로 일반화 해야하는 경우도 생길 수 있음. 

    클라이언트가 최상위 인터페이스 Component를 참조하기 때문에 클라이언트 코드를 변경하지 않고 새로운 Element(Leaf Node, Composite)을 추가할 수 있다는 장점이 있다. 

    반면 트리구조를 만들기 위해 지나친 일반화가 발생할 수 있는데, 이 경우는 적용하면 안되는 곳에 디자인 패턴을 적용하고 있다는 신호일 수 있다. 지나친 일반화의 냄새로는 트리 구조 내에서 런타임에 타입을 체크하는 경우가 있을텐데 이런 경우면 지나친 일반화를 하고 있다고 이해할 수 있다. 예를 들어 isInstance(Bag, Item) 같은 것들을 사용하는 순간들이다. 

    댓글

    Designed by JB FACTORY