컴포짓 (Composite) 패턴

weekbelt·2022년 12월 11일
0

1. 패턴 소개

컴포짓 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있습니다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있습니다.

위의 컴포짓 디자인패턴의 구조를 보면 Client는 Component인터페이스에서 정의한 메서드만을 사용하도록 하고 개발자가 생성할 객체들은 Leaf 혹은 Composite타입으로 구현합니다. Leaf는 가장 primitive한 단위가 되고 Composite은 primitive한 타입들을 group으로 가지고 있는 객체입니다. 따라서 Composite은 여러개의 Component를 배열 혹은 리스트로 가지고 있습니다. 이 그룹에 속한 chilent의 타입들도 Leaf가 아닌 Component이고 Componenet에 정의된 opertaion을 제공하기 때문에 일관된 연산을 수행할 수 있습니다.

컴포짓패턴을 적용하기 전 예시 코드를 살펴보겠습니다. Client에서 도란검과 체력물약이라는 아이템을 생성하고 그것을 가방에 담는 코드입니다.

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);
}

그렇다면 아이템 하나를 출력하는 메서드와 가방에 담긴 아이템의 전체 가격을 출력하는 메서드를 위와 같이 각각 구현해야 합니다. 가격을 출력하는 메서드들이 Client클래스에 남게 되기 때문에 Client입장에서 가방의 각각의 아이템과 가격정보를 알게됩니다. 하지만 Client클래스에서 이런 정보를 알아야할 필요가 있을까 객체지향적으로 생각해봐야합니다. 가방안에 가방이 들어있거나 가방이 2개가 있다면 또 다른 메서드를 Client에 구현을 해야하고 Client가 가격을 출력하는 모든 로직을 알고있어야한다는 뜻이고 결국 코드변경이 생긴다는 뜻입니다. 이런 객체지향적이지 않는 부분을 컴포짓 패턴을 적용해서 개선할 수 있습니다.

2. 패턴 적용하기

위에서 객체지향과는 거리가 먼 코드를 컴포짓 패턴을 적용해서 개선해 보도록 하겠습니다. 먼저 컴포짓 패턴에는 Componenet라는 공통된 인터페이스를 정의해야합니다. Component를 정의하고 위에서 가격을 구하는 메서드를 정의했기때문에 getPrice라고 공통적인 메서드를 정의합니다. 이 Component자체가 중요하다기 보다는 getPrice라는 공통된 operation을 인터페이스에 정의되어 있다는 것이 중요합니다.

public interface Component {

    int getPrice();

}

그 다음으로 Item과 Bag을 정의해 보도록 하겠습니다. Item은 계층구조에서 제일 primitive한 요소 이기 때문에 Leaf역할을 합니다. Item에서는 Component인터페이스의 getPrice에서 Item의 가격을 리턴하도록 재정의하면 됩니다.

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;
    }
}

Bag은 Component타입들을 가지고 있는 Composite역할을 합니다. Compsite도 Componenet타입이므로 Bag은 Component인터페이스의 getPrice로 해당 Component타입들의 가격의 합을 구하도록 재정의합니다.

public class Bag implements Component {

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

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

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

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

결국 Item, Bag모두 Component타입이기 떄문에 Bag안에 Componenet타입의 리스트의 요소들이 Bag이던 Item이던 상관없이 가격을 구할때 일관되게 getPrice로 호출할 수 있습니다. 그래서 기존에 Bag에 있는 가격을 구하는 메서드가 Client에 정의되어 있었던 것이 Bag으로 이동하게 됩니다.

가격을 구하는 책임이 Client에서 Bag으로 이동하면서 좀 더 객체지향적으로 변경되었습니다. 이것이 객체지향적으로 맞는 이유는 Clienet가 Bag의 가격을 구하는지 Item의 가격을 구하는지 지나치게 많은걸 알고있지 않고 가격을 구할때 그냥 getPrice메서드만 호출하면 되기 때문입니다.

이제 Client코드에서 사용해 보도록 하겠습니다.

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());
    }
}

기존엔 Item의 가격과 Bag가격을 구할 때 client에서 각각 메서드를 정의했지만 패턴 적용후에 Componenet라는 공통 인터페이스를 받아서 getPrice메서드만 호출하면 Item과 Bag타입에 상관없이 가격을 구할수 있습니다.

컴포짓 패턴을 적용하기전 Client에서 Item과 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);
}

컴포짓 패턴을 적용 후 Client에서 가격 출력을 정의한 메서드

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

비교를 해보면 가격을 구하는 로직이 Client에서 직접 구현되어 있던부분이 Component에 위임되었다는것을 확인할 수 있습니다. 전체나 부분이나 Client에서는 동일하게 취급이 가능해졌습니다.

3. 결론

컴포짓 패턴은 그룹 전체와 개별 객체를 동일하게 처리할 수 있는 패턴입니다. 컴포짓 패턴을 적용하면 복잡한 트리구조를 Component인터페이스에 정의한 메서드만 요청해서 편리하게 사용할 수 있습니다. 결국 클라이언트 코드를 변경하지 않고 새로운 엘리먼트타입을 추가할 수 있습니다. 하지만 트리구조가 되기 때문에 지나치게 일반화 될수 있다는점을 유의해야합니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글