본문 바로가기
Develop/Design

[Design Pattern] Composite 패턴 정리

by 코딩의성지 2022. 10. 11.

 Composite pattern

 

오늘은 Composite Pattern 에 대해 정리를 해 두고자한다.

 

해당 패턴은 전체와 부분을 하나의 단위로 추상화해야할 경우 사용된다.

클라이언트 입장에서 메시지 수신자가 부분인지 전체인지에 상관없이 동일한 메시지를 통해 동일한 방식으로 대상과 상호작용해야할 경우 사용하는 패턴이다.

 

아래는 Composite Pattern 을 다이어그램으로 그린 형태이다.

Composite pattern에서 협력에 참여하는 역할과 책임을하는 구성요소를 살펴보자.

Component 는 클라이언트와 협력할 수 있는 공용인터페이스를 정의하는 역할을 수행한다. 예시로 든 다이어그램에서는 추가, 제거, 포함된 하위 컴포넌트 반환 등의 역할을 수행한다.

Leaf  는 공용 인터페이스에 대한 오퍼레이션 호출에 응답할 수 잇는 기본적인 행위를 구현한다.

Composite 는 외부로 부터 부분에 대한 세부사항을 감추고 포함된 부분을 하나의 단위로 행동하게 하는 역할을 수행한다.

 

그리고 Client  component  에게 메시지를 요청함으로써 패턴에 협력한다.

 

이렇게만 설명하면 조금 어려울 수 있으니, 간단한 예제로 설명을 추가하고자 한다.

 

패키지 상품을 판매하는 여행사 시스템을 만든다고 가정하자.

패키지 상품은 몇 가지 액티비티를 섞어서 구성할 수가 있다.

 

해당 시스템을 개발하는 개발자는 아래와 같이 시스템을 설계 했다.

 

public class Accommodation {
    private int price;
    private String description;

    public Accommodation(int price, String description) {
        this.price = price;
        this.description = description;
    }

    public int getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }
}

 

public class Meal {
    private int price;
    private String description;

    public Meal(int price, String description) {
        this.price = price;
        this.description = description;
    }

    public int getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }
}

 

 

public class Activity {
    private int price;
    private String description;

    public Activity(int price, String description) {
        this.price = price;
        this.description = description;
    }

    public int getPrice() {
        return price;
    }

    public String getDescription() {
        return description;
    }
}

 

 

public class TravelProduct {
    private Accommodation accommodation;
    private Meal meal;
    private Activity activity;

    public void addAccommodation(Accommodation accommodation) {
        this.accommodation = accommodation;
    }

    public void addMeal(Meal meal) {
        this.meal = meal;
    }

    public void addActivity(Activity activity) {
        this.activity = activity;
    }

    public int getPrice() {
        int sum =0;
        int accommodationPrice = accommodation.getPrice();
        int mealPrice = meal.getPrice();
        int activityPrice = activity.getPrice();

        sum = accommodationPrice + mealPrice + activityPrice;

        return sum;
    }

    public String getDescription() {
        String travelDescription = "전체 여행 설명\n";
        String accommodationDescription = accommodation.getDescription();
        String mealDescription = meal.getDescription();
        String activityDescription = activity.getDescription();

        travelDescription += accommodationDescription + mealDescription + activityDescription;

        return travelDescription;
    }
}

 

 

public class Client {
    public static void main(String[] args) {
        Accommodation accommodation = new Accommodation(120000, "ㅇㅇ 리조트에서 1박 숙박 (아침 조식 제공). \n");
        Meal meal = new Meal(15000, "ㅁㅁ 식당에서 쭈꾸미 정식 (점심식사) \n");
        Activity activity = new Activity(30000, "ㄱㄱ 계곡에서 래프팅\n");

        TravelProduct travelProduct = new TravelProduct();
        travelProduct.addAccommodation(accommodation);
        travelProduct.addMeal(meal);
        travelProduct.addActivity(activity);

        int travelProductPrice = travelProduct.getPrice();
        String travelProductDescription = travelProduct.getDescription();

        System.out.println("================가격===============");
        System.out.println(travelProductPrice);
        System.out.println("================설명===============");
        System.out.println(travelProductDescription);

    }
}

출력 결과

 

이는 합성관계를 활용한 설계인데, 이와 같이 설계를 하면 전체 객체의 생명주기와 부분 객체의 생명주기가 서로 의존적이다. 즉 전체 객체가 사라지면 부분객체도 사라지게 된다.

 

이렇게 단순하게 합성관계를 이용해 설계를 진행하면 두가지 문제가 존재한다.

 

첫번째는 여러개의 동일한 타입의 컴포넌트의 추가가 불가능하다. 만약에 여행상품에 점심 뿐만아니라 저녁이 추가되야하면 어떻게 되겠는가? 또한 1박 일정이 아니고 2박일정이면 숙소정보가 하나더 추가 되어야한다. 즉 동일한타입의 여러 객체를 여행상품에 포함시킬수 있어야한다.

 

두번재 문제는 다른 컴포넌트가 추가되어야할 때이다. 여행 상품 중 관람이라는 컴포넌트가 추가되면 어떻게 되겠는가? 관람이라는 요소를 추가하기 위해 아래와 같은 과정을 거쳐야한다.

 

1. 관람을 뜻하는 View 클래스 추가. 

2. TravelProduct에 새로운 필드인  View를 추가하고 , setter 메서드인 addView를 추가.

3. TravelProduct 에서 View 정보가 추가되도록 getPrice 메서드와 getDescription 메서드 수정.

 

이렇게 수정하면 객체지향 원칙중 하나인 OCP 원칙을 위반하게 된다.

 

이러한 점을 보완하기위해 Composite Pattern 을 활용할 수 있다.

설계를 간단하게 설명하면

Component는 TravelComponent가,

Lef는 Accomodation, Meal, Activity, View 가,

Composite는 TravelProduct가 역할을 담당하게 된다.

 

여기서 TravelProduuct 클래스는 TravelComponent의 하위 클래스이면서 복수 개의 TravelComponent를 가질 수 있다

그리고 addComponent를 통해 컴포넌트를 추가할 수 있다. 

View라는 새로운 컴포넌트도 단순히 TravelComponent의 하위 클래스로 구현했기에 OCP 원칙을 철저히 지킬 수 있다. 즉 부분객체가 추가되거나 삭제되어도 전체클래스 코드의 변경이 필요없나는 말이다.

이렇게 전체-부분 관계를 갖는 객체 사이에서 Composite Pattern은 유용하게 동작한다.

 

예제 코드를 마지막으로 글을 마무리 하도록 하겠다.

public abstract class TravelComponent {
    public abstract int getPrice();
    public abstract String getDescription();
}

 

 

public class Accommodation extends TravelComponent{
    private int price;
    private String description;

    public Accommodation(int price, String description) {
        this.price = price;
        this.description = description;
    }

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

    @Override
    public String getDescription() {
        return description;
    }
}

 

 

public class Meal extends TravelComponent{
    private int price;
    private String description;

    public Meal(int price, String description) {
        this.price = price;
        this.description = description;
    }

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

    @Override
    public String getDescription() {
        return description;
    }
}

 

 

public class Activity  extends TravelComponent{
    private int price;
    private String description;

    public Activity(int price, String description) {
        this.price = price;
        this.description = description;
    }

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

    @Override
    public String getDescription() {
        return description;
    }
}

 

 

public class View  extends TravelComponent{
    private int price;
    private String description;

    public View(int price, String description) {
        this.price = price;
        this.description = description;
    }

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

    @Override
    public String getDescription() {
        return description;
    }
}

 

 

public class TravelProduct extends TravelComponent{

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

    public void addComponent(TravelComponent component) {components.add(component);}

    public void removeComponent(TravelComponent component) {components.remove(component);}

    @Override
    public int getPrice() {
        int price = 0;
        for(TravelComponent travelComponent : components) {
            price += travelComponent.getPrice();
        }
        return price;
    }

    @Override
    public String getDescription() {
        String travelDescription = "전체 여행 설명\n";
        for(TravelComponent travelComponent : components) {
            travelDescription += travelComponent.getDescription();
        }
        return travelDescription;
    }
}

 

 

public class Client {
    public static void main(String[] args) {
        Accommodation accommodation = new Accommodation(120000, "ㅇㅇ 리조트에서 1박 숙박 (아침 조식 제공). \n");
        Meal meal1 = new Meal(15000, "ㅁㅁ 식당에서 쭈꾸미 정식 (점심식사) \n");
        Activity activity = new Activity(30000, "ㄱㄱ 계곡에서 래프팅\n");
        Meal meal2 = new Meal(25000, "ㅂㅂ 횟집에서 회정식 (저녁식사) \n");
        View view = new View(8000, "ㅊㅊ 민속 박물관 방문\n");

        TravelProduct travelProduct = new TravelProduct();
        travelProduct.addComponent(accommodation);
        travelProduct.addComponent(meal1);
        travelProduct.addComponent(activity);
        travelProduct.addComponent(meal2);
        travelProduct.addComponent(view);

        int travelProductPrice = travelProduct.getPrice();
        String travelProductDescription = travelProduct.getDescription();

        System.out.println("================가격===============");
        System.out.println(travelProductPrice);
        System.out.println("================설명===============");
        System.out.println(travelProductDescription);

    }
}

결과 화면

 

반응형

댓글