컴포지트 패턴(Composite Pattern)

Minjae An·2023년 12월 4일
0

설계패턴

목록 보기
7/7

컴포지트 패턴(Composite Pattern)

👨🏼‍💻 컴포지트 패턴은 컴포지트(복합 객체)와 단일 객체를 동일한 컴포넌트로
취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록
하는 구조 패턴이다.

이 패턴은 전체-부분의 관계를 가지는 객체들 사이의 관계를 트리 계층 구조로
정의해야 할 때 유용하다. 파일 시스템 구조를 떠올려보면 쉽게 이해할 수 있다.

폴더(디렉터리) 내에는 파일이 들어있거나, 파일을 포함한 또 다른 폴더가 들어있을 수 있다. 이처럼 복합적인 형태를 가리켜 Composite 객체라 부른다. 반면 파일은 단일
객체이기 때문에 Leaf 객체라 불린다.

컴포지트 패턴은 이런 폴더와 파일을 동일 타입으로 취급하여 구현을 단순화 시키는 것이 목적이다.

계층 구조를 구현하다 보면 복잡해질 수 도 있는 복합 객체를 재귀 동작을 통해 하위
객체들에게 작업을 위임하는 형태로 구현한다. 그러면 복합 / 단일 객체를 대상으로
동일 작업을 적용할 수 있어 두 객체를 구분할 필요가 줄어든다.

정리하자면, 컴포지트 패턴은 그릇과 내용물을 동일시해서 재귀적 구조를 만들기 위한 디자인 패턴이라 말할 수 있다.

컴포지트 패턴 구조


이 패턴의 핵심은 Composite와 Leaf가 동시에 구현하는 operation() 인터페이스
추상 메서드를 정의하고, Composite 객체의 operation() 메서드는 스스로를
호출하는 재귀 형태로 구현하는 것이다. 파일 시스템같은 트리 구조를 생각해보면,
재귀적으로 반복되는 형식이 나타난다.

따라서 단일체와 복합체를 동일 개체로 취급하여 처리하기 위해 재귀 함수 원리를
이용한다.

택배 비용 계산하기

각종 물품을 담는 박스가 존재할 때 객체간 관계가 다음과 같은 구조로 형상화될 수
있다.

Box

public class Box {
    private List<Trousers> trousers = new ArrayList<>();
    private List<Socks> socks = new ArrayList<>();

    public int price() {
        int tPrice = trousers.stream()
                .mapToInt(Trousers::price)
                .sum();

        int sPrice = socks.stream()
                .mapToInt(Socks::price)
                .sum();

        return tPrice + sPrice;
    }

    public void addSocks(Socks s) {
        socks.add(s);
    }

    public void addTrousers(Trousers t) {
        trousers.add(t);
    }
}

Socks

public class Socks {
    private int weight;

    public Socks(int weight) {
        this.weight = weight;
    }

    public int price() {
        return this.weight / 100 * 200;
    }
}

Trousers

public class Trousers {
    private int weight;

    public Trousers(int weight) {
        this.weight = weight;
    }

    public int price() {
        return this.weight / 100 * 200;
    }
}

택배 아이템 추가

다음과 같이 택배 아이템이 추가되는 상황이 마주한다면?

public class Box {
    private List<Trousers> trousers = new ArrayList<>();
    private List<Socks> socks = new ArrayList<>();
    private List<Golds> golds = new ArrayList<>();

    public int price() {
        int tPrice = trousers.stream()
                .mapToInt(Trousers::price)
                .sum();

        int sPrice = socks.stream()
                .mapToInt(Socks::price)
                .sum();

        int gPrice = golds.stream()
                .mapToInt(Golds::price)
                .sum();

        return tPrice + sPrice + gPrice;
    }

    public void addSocks(Socks s) {
        socks.add(s);
    }

    public void addTrousers(Trousers t) {
        trousers.add(t);
    }

    public void addGolds(Golds g) {
        golds.add(g);
    }
}

기존 Box 에 추가된 아이템 타입의 필드를 추가하고, 가격을 계산하는 로직도
수정해야 한다. 따라서 OCP를 위배하게 된다.

한편, 만약 박스 내에 박스가 포함될 수 있는 형태도 가능하다면 로직을 어떻게
변경해야 할까?

Box

public class Box {
    private List<Trousers> trousers = new ArrayList<>();
    private List<Socks> socks = new ArrayList<>();
    private List<Golds> golds = new ArrayList<>();
    private List<Box> boxes = new ArrayList<>();

    public int price() {
        int tPrice = trousers.stream()
                .mapToInt(Trousers::price)
                .sum();

        int sPrice = socks.stream()
                .mapToInt(Socks::price)
                .sum();

        int gPrice = golds.stream()
                .mapToInt(Golds::price)
                .sum();

        int bPrice = boxes.stream()
                .mapToInt(Box::price)
                .sum();

        return tPrice + sPrice + gPrice + bPrice;
    }

    public void addSocks(Socks s) {
        socks.add(s);
    }

    public void addTrousers(Trousers t) {
        trousers.add(t);
    }

    public void addGolds(Golds g) {
        golds.add(g);
    }

    public void addBox(Box b) {
        boxes.add(b);
    }
}

Client

public class Client {
    public static void main(String[] args) {
        Box box = new Box();
        Socks s1 = new Socks(100);
        Socks s2 = new Socks(200);
        Trousers t1 = new Trousers(600);
        box.addSocks(s1);
        box.addSocks(s2);
        box.addTrousers(t1);
        System.out.println(box.price());

        Box box2 = new Box();
        Golds g1 = new Golds(800);
        box2.addBox(box);
        box2.addGolds(g1);
        System.out.println(box2.price());
    }
}

이 경우에도 OCP를 위배하게 된다.

해결책

단순히 Object 를 이용해 포괄적으로 필드를 구성하고 instanceof 연산자로
객체를 구분하여 가격을 계산하는 방법을 생각할 수 있지만 그 경우에도 새로운 물건
추가시 분기문이 추가되기 때문에 OCP에 위배된다.

따라서 다음과 같은 사실을 포함하는 상위 개념은 ParceItem 을 설정할 수 있다.

  • 택배로 보낼 수 있는 항목
  • Box 는 택배로 보낼 수 있는 항목을 여러 개 가질 수 있다.

ParceItem

public abstract class ParceItem {
    protected int weight;

    public ParceItem(int weight) {
        this.weight = weight;
    }

    public abstract int price();
}

Box

public class Box extends ParceItem {
    private List<ParceItem> items = new ArrayList<>();

    public Box(int weight) {
        super(weight);
    }

    @Override
    public int price() {
        return items.stream()
                .mapToInt(ParceItem::price)
                .sum();
    }

    public void addItem(ParceItem item) {
        items.add(item);
    }
}

Socks

public class Socks extends ParceItem {
    public Socks(int weight) {
        super(weight);
    }

    @Override
    public int price() {
        return weight / 100 * 200;
    }
}

컴포지트 패턴 특징

패턴 사용 시기

  • 데이터를 다룰 때 계층적 트리 표현을 다루어야 할 때
  • 복잡하고 난해한 단일 / 복합 객체 관계를 간편히 단순화하여 균일하게 처리하고 싶을 때

패턴 장점

  • 단일체와 복합체를 동일하게 여기기 때문에 묶어서 연산하거나 관리할 때 편리하다.
  • 다형성 재귀를 통해 복잡한 트리 구조를 보다 간편하게 구성할 수 있다.
  • 수평적, 수직적 모든 방향으로 객체를 확장할 수 있다.
  • 새로운 Leaf 클래스를 추가하더라도 클라이언트는 추상화된 인터페이스만을 사용하기 때문에 OCP를 준수한다. (단일 부분 확장이 용이)

패턴 단점

  • 재귀 호출 특징 상 트리의 깊이가 깊어질 수록 디버깅이 어려워진다.
  • 설계가 지나치게 범용성을 가지기에 새 요소를 추가할 때 복합 객체에서 구성
    요소에 제약을 가지기 힘들다.
  • 예를 들어 계층 구조에서 Leaf 객체와 Composite 객체들을 모두 동일한
    인터페이스로 다루어야하는데, 이 공통 인터페이스 설계가 까다로울 수 있다.
    • 복합 객체가 가지는 부분 객체의 종류를 제한할 필요가 있을 때
    • 수평적 방향으로만 확장이 가능하도록 Leaf를 제한하는 Composite를 만들 때

참고

profile
도전을 성과로

0개의 댓글