본문 바로가기
쿤즈 Dev/Design pattern

[Design Pattern] 컴포짓 패턴: 복합 객체를 구성하여 계층 구조를 만드는 패턴

by Koonz:) 2024. 8. 30.
728x90

컴포짓 패턴(Composite Pattern)은 구조 디자인 패턴 중 하나로, 객체들을 트리 구조로 구성하여 부분 또는 전체 계층을 표현하는 패턴입니다. 이는 개별 패턴과 복합 객체를 동일하게 취급할 수 있도록 합니다.

 

이번 글에서는 컴포짓 패턴의 개념과 필요성, 그리고 이를 자바로 구현하는 방법을 알아볼게요. 마찬가지로 커피 주문 시스템에서 컴포짓 패턴을 적용한 예제와 함께 진행해 보겠습니다.


🧑🏻‍💻 컴포짓 패턴 Composite Pattern

컴포짓 패턴은 개별 객체와 복합 객체를 동일하게 다룰 수 있는 구조를 만드는 디자인 패턴입니다. 객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴입니다.

컴포짓 패턴

 

컴포짓 패턴에서는 Component를 인터페이스로 만들고 클라이언트에서는 Component를 사용합니다. 되도록이면 Leaf를 사용하지는 않습니다. 이는 예제를 통해서 조금 더 자세하게 알아볼게요.


🛠️ 컴포짓 패턴의 필요성

컴포짓 패턴은 다음과 같은 경우에 필요합니다.

트리 구조 표현

복합 객체를 트리 구조로 표현하여, 개별 객체와 복합 객체를 동일하게 다룰 수 있습니다.

유연한 구조

객체를 자유롭게 추가, 삭제, 수정할 수 있어 구조가 유연해집니다.

단순한 인터페이스

클라이언트가 개별 객체와 복합 객체를 동일한 인터페이스로 다룰 수 있어 코드가 단순해집니다.


🚀 컴포짓 패턴 적용하기

컴포짓 패턴을 적용하기 위해서 커피 주문 시스템을 예제로 만들어 볼게요. 이 예제에서는 커피와 추가 옵션(에스프레소, 우유, 시럽)을 트리 구조로 구성하여 표현합니다.

 

🤔 컴포짓 패턴 적용 전

컴포짓 패턴을 적용하기 전 커피 주문 시스템을 이용해서 간단하게 총 금액을 계산하는 프로그램을 만들어 볼게요.

가장 먼저 Coffee 클래스를 만들어 보겠습니다.

package com.koonsland.designpatterns.structural.composite.example;

import java.math.BigDecimal;

public class Coffee {
    private String name;
    private BigDecimal price;

    public Coffee(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "Coffee{" + "name='" + name + '\'' + ", price=" + price + '}';
    }
}

 

Coffee 클래스는 이름을 나타내는 name 필드와 가격을 저장하는 price 필드를 가지고 있습니다. 그리고 초기화 시 사용하는 생성자와 getter를 만들어 두고 확인을 위해 toString() 메서드를 만들어 두었습니다.

 

다음은 커피를 주문하는 CoffeeOrder 클래스를 만들어 줄게요.

package com.koonsland.designpatterns.structural.composite.example;

import java.util.ArrayList;
import java.util.List;

public class CoffeeOrder {
    List<Coffee> orderList = new ArrayList<>();

    public List<Coffee> getOrderList() {
        return orderList;
    }

    public void addOrder(Coffee coffee) {
        this.orderList.add(coffee);
    }

    public void removeOrder(Coffee coffee) {
        this.orderList.remove(coffee);
    }
}

 

CoffeeOrder 클래스는 주문 정보들의 리스트를 저장하는 orderList 필드를 가지고 있으며 주문이 들어올 경우 addOrder를 통해서 주문정보들을 담고, removeOrder를 통해서 주문 정보를 삭제합니다.

 

이제 클라이언트 프로그램을 만들어서 테스트 해볼게요.

 

package com.koonsland.designpatterns.structural.composite.example;

import java.math.BigDecimal;

public class Client {
    public static void main(String[] args) {
        Coffee americano = new Coffee("아메리카노", BigDecimal.valueOf(4000));
        Coffee latte = new Coffee("카페라떼", BigDecimal.valueOf(5000));

        CoffeeOrder coffeeOrder = new CoffeeOrder();
        coffeeOrder.addOrder(americano);
        coffeeOrder.addOrder(latte);

        Client client = new Client();
        client.showPrice(coffeeOrder);
    }

    private void showPrice(CoffeeOrder coffeeOrder) {
        BigDecimal totalPrice = coffeeOrder.getOrderList()
                .stream()
                .map(Coffee::getPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        System.out.println("Total Price: " + totalPrice);
    }
}

 

Client에서는 Coffee 인스턴스를 만들어 주었습니다. 아메리카노 1개와 카페라떼 1개를 만들어 주었는데요. 각각 4천원과 5천원이라는 값을 함께 넣어서 초기화해 주었습니다. 그리고 이렇게 초기화된 Coffee 인스턴스를 각각 coffeeOrder 인스턴스에 추가하여 가격을 확인해 보았습니다.

 

가격을 확인하기 위해서 별도의 메서드를 만들어서 가격을 가져왔어요. 이제 이 가격을 가져오는 부분을 컴포짓 패턴을 적용해서 만들어 볼게요.

 

😀 컴포짓 패턴 적용

컴포짓 패턴을 적용하기 위해서 기존 클래스는 역시 대부분 그대로 둡니다. 다만 CoffeeComponent 인터페이스를 만들어서 메서드 하나를 구현체로 만들어 줄게요.

 

package com.koonsland.designpatterns.structural.composite.after;

import java.math.BigDecimal;

public interface CoffeeComponent {
    BigDecimal getPrice();
}

 

가격을 가져오는 부분을 컴포짓 패턴으로 만들기 위해서 CoffeeComponent 인터페이스에 getPrice() 메서드를 만들어 주었습니다. 그리고 Coffee 클래스는 다음과 같이 어노테이션을 하나 붙여줍니다.

 

package com.koonsland.designpatterns.structural.composite.after;

import java.math.BigDecimal;

public class Coffee implements CoffeeComponent {
    private String name;
    private BigDecimal price;

    public Coffee(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Coffee{" + "name='" + name + '\'' + ", price=" + price + '}';
    }
}

 

getPrice() 메서드는 기존에 구현된 메서드이며 @Override 어노테이션을 붙여서 구현된 메서드임을 표시해 줍니다. 붙여주지 않아도 되지만 어떤 메서드가 구현되어 있는지 표현을 해주는 의미이며 이 부분으로 이해서 구현체가 컴파일 과정에서 어떤 인터페이스인지 알게 되는 분이므로 붙여주는 습관을 들이는 것이 좋습니다.

 

다음은 CoffeeOrder 클래스를 조금 수정해 주도록 하겠습니다.

package com.koonsland.designpatterns.structural.composite.after;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class CoffeeOrder implements CoffeeComponent {
    private List<CoffeeComponent> components = new ArrayList<>();

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

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

    @Override
    public BigDecimal getPrice() {
        return this.components
                .stream()
                .map(CoffeeComponent::getPrice)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

 

동일하게 CoffeeComponent 인터페이스를 상속받습니다. 다만 내부에 Coffee 클래스였던 부분은 모두 CoffeeComponent 인터페이스로 만들어 줍니다. 추가 삭제를 하는 부분 역시 모두 인터페이스로 추상화된 클래스를 받을 수 있게 해 두었습니다.

마지막으로 @Override 하여 가격을 모두 합산하는 메서드의 구현체를 만들어 줍니다.

 

이제 클라이언트에서 테스트하도록 할게요.

package com.koonsland.designpatterns.structural.composite.after;

import java.math.BigDecimal;

public class Client {
    public static void main(String[] args) {
        Coffee americano = new Coffee("아메리카노", BigDecimal.valueOf(4000));
        Coffee latte = new Coffee("카페라떼", BigDecimal.valueOf(5000));

        CoffeeOrder coffeeOrder = new CoffeeOrder();
        coffeeOrder.addComponent(americano);
        coffeeOrder.addComponent(latte);

        System.out.println(coffeeOrder.getPrice());
    }

}

 

 

하나의 getPrice() 메서드를 이용해서 이를 상속받은 모든 클래스에서 가격 정보를 가져올 수 있는 방식입니다. CoffeeComponent 인터페이스를 모든 클래스에서 상속받고 각자 구현하면서 계층구조의 모든 가격 정보를 합산할 수 있는 형태의 구조가 컴포짓 패턴의 대표적인 예제입니다.


🎵 결론

컴포짓 패턴은 객체들을 트리 구조로 구성하여 부분 또는 전체 계층을 표현할 수 있는 유용한 디자인 패턴입니다. 이를 통해 개별 객체와 복합 객체를 동일하게 다룰 수 있어 코드의 유연성과 확장성을 높일 수 있습니다. 이번 글에서는 커피 주문 시스템과 스프링 프레임워크에서 컴포짓 패턴을 적용한 예제를 통해서 컴포짓 패턴의 개념과 구현 방법을 살펴보았습니다. 컴포짓 패턴을 사용하면 시스템의 복잡성을 줄이고, 유지보수성을 높일 수 있습니다.

 

더 궁금하신 부분은 댓글로 남겨주시고 이 글이 도움이 되셨으면 합니다. 

댓글