본문 바로가기
Engineering/SW Design

[디자인 패턴] 구조패턴 - 컴포짓

by 쿨쥰 2024. 4. 26.

구조패턴 - 컴포짓

 


이전 글 : 구조패턴 - 브릿지

2024.03.28 - [Engineering/SW Design] - [디자인 패턴] 구조패턴 - 브릿지

 

[디자인 패턴] 구조패턴 - 브릿지

구조패턴 - 브릿지 이전 글 : 구조패턴 - 어댑터 2024.03.25 - [Engineering/SW Design] - [디자인 패턴] 구조패턴 - 어댑터 [디자인 패턴] 구조패턴 - 어댑터 구조패턴 - 어댑터 이전 글 : 구조패턴 - 싱글턴 202

skidrow6122.tistory.com

 

 

요약

클라이언트가 복합 객체 (group of object) 나 단일 객체 에 연연하지 않고 동일하게 취급하는 것을 목적으로 한다.

트리 관계의 객체를 처리 하기 위함이 목적이며, Data modeling 에서의 BOM (bill of material) 개념과 흡사하다고 보면 된다.

궁극적으로 composite 의 의도는 트리구조로 객체를 작성하여, 전체-부분 관계를 표현하는 것이다.

트리하면 무조건 나오는 개념인 리프 노트와 브랜치가 적용되며, 여기서 파생되는 복잡성에 따른 에러를 해결하기 위해 인터페이스를 활용한다.

대표적인 예시로서 Directory - File 관계에서 전체-부분 관계를 효율적으로 정의 할 수 있다.

패턴은 컴포넌트 (인터페이스) + 리프 구현 + 컴포짓 구현 총 3가지 레이어로 나뉜다.

  • component
    • 클라이언트가 composition 내의 오브젝트를 다루기 위해 제공되는 인터페이스
    • interface 또는 abstract 클래스로 정의되며 모든 오브젝트들에게 공통되는 메소드를 정의 해야 함
  • Leaf
    • composition 내 오브젝트들의 행동을 정의
    • 다른 컴포넌트들에 대해 참조를 가지면 안됨
  • Composite
    • Leaf 객체들로 이루어져 있고 컴포넌트 내 명령들을 구현

키워드 : 트리 / 컴포넌트(인터페이스) / 리프 / 컴포짓 / 재귀 객체 구조 / 상품과 상품 옵션

 

 

 

문제

제품들상자들이라는 두가지 유형의 객체들이 있다고 가정한다.

상자에는 여러개의 제품들과 여러개의 더 작은 상자들이 포함될 수 있다.

이 작은 상자들은 또 일부 제품들을 또는 더 작은 상자들 등을 담을 수 있다.

이러한 상황에서 각 제품은 클래스라고 치고, 이러한 클래스들을 사용하는 주문 시스템이 있다고 가정해보자.

주문들에는 포장이 없는 단순한 제품들과, 제품들로 채워진 상자들 또는 더 작은 단위의 상자들이 포함 될 수 있다.

이때 주문의 총 가격을 어떻게 계산 할 수 있을까?

별 생각 없이 이문제에 접근하자면, 모든 상자를 일단 다 풀어 헤친 후에 내부 제품 가격의 합을 구하는 것을 고려해볼 수 있지만,,,

이 방법은 당연히 루프를 굉장히 많이 돌려야 하므로 효율적이지 않다.

 

 

해결책

컴포짓 패턴에서는 총 가격을 계산하는 메서드를 선언하는 공통 인터페이스를 통해서 제품들과 상자들 클래스를 풀어나갈 수 있도록 해준다.

  • 제품들의 경우, 이 메서드은 단순히 제품 가격을 반환한다.
  • 상자들의 경우, 이 메서드는 상자에 포함된 각 제품의 항목을 살펴보고 가격을 확인 한뒤, 해당 상자의 총 가격을 반환한다.
    • 만약 이 항목들 중 하나가 더 작은 상자라면, 메서드는 해당 상자의 모든 내부 구성품의 가격이 계산될 때 까지 내용물을 살펴본다.
    • 여기에 최종가격에 만약 포장 비용같은것이 붙는다 해도 특수 로직으로 넣을 수 있다.

이 접근 방식은 더 이상 트리를 구성하는 객체들의 구상 클래스들에 대해 신경 쓸 필요도, 또 물건이 단순한 제품인지 내용물이 있는 상자인지 알 필요도 없다는 점이다.

단순히 공통 인터페이스를 통해 모두 같은 방식으로 처리하면 된다.

메서드를 호출만하면 객체들 자체가 요청을 트리 아래로 전달하기 때문이다.

 

 

구조

  • Component 에 해당하는 interface 에서 공통의 함수 (아까 예시에서의 가격 계산 함수 등) 을 제공
  • Leaf 는 더이상 하위 상자가 없는 ‘제품들’ 에 해당
  • Composite 는 그 자체가 하위 상자로 구성된 ‘상자들’ 에 해당

 

 

Hands on

Shape 이라는 모양 인터페이스인 component interface 가 존재하고, 여기에 컬러를 받아서 draw 하는 함수가 있다고 가정하자.

이 인터페이스는 leaf 로서 삼각형, 사각형, 원형을 각각 구현 할 수 있다.

public interface Shape {
    fun draw(paintColor: String)
}
  • Component 에서는 leaf 와 Composite의 공통이 되는 메소드가 정의 된다.
  • 따라서, Shape 인터페이스 내 각각 도형에 색깔을 칠하는 draw() 메소드를 정의 하였다.

 

public class Triangle : Shape {
    override fun draw(paintColor: String) = println("삼각형 색깔" + paintColor)
}

public class Square : Shape {
    override fun draw(paintColor: String) = println("사각형 색깔" + paintColor)
}

public class Circle : Shape {
    override fun draw(paintColor: String) = println("원형 색깔" + paintColor)
}
  • Leaf 객체들은 아래에 구현할 Composite에 포함되는 요소이다.

 

public class CompositeDrawing(val shapes: ArrayList<Shape> = ArrayList()) : Shape {
    override fun draw(paintColor: String) {
        for (shape in shapes) {
            shape.draw(paintColor)
        }
    }

    fun add(s: Shape) = this.shapes.add(s)
    fun remove(s: Shape) = this.shapes.remove(s)
    fun clear(s: Shape) {
        println("모든 도형 삭제")
        this.shapes.clear()
    }
}
  • Composite객체는 Leaf 객체를 포함하고 있고, Component 인 Shape를 구현한다.
  • 또한, Leaf 그룹에 대해서 add, remove, clear 할 수 있는 메소드를 클라이언트에 제공 한다.
  • 여기서 중요한 점은 Composite의 객체 또한 Component를 구현해야 한다는 것이다. (draw())
    • 이렇게 해야만 클라이언트가 Composite 객체에 대해 다른 leaf 와 동일하게 취급할 수 있다.

 

fun main(){
    // 삼각형 leaf들을 초기화
    val triangle1 = Triangle()
    val triangle2 = Triangle()
    val triangle3 = Triangle()

    // 원형 leaf들을 초기화
    val circle1 = Circle()
    val circle2 = Circle()
    val circle3 = Circle()

    // component 를 초기화 (상자)
    val shape = CompositeDrawing()
    val shape1 = CompositeDrawing()
    val shape2 = CompositeDrawing()

    // 첫번째 상자에 삼각형 두개, 원형 두개를 담고 파란색으로 색을 칠한다.
    shape1.add(triangle1)
    shape1.add(triangle2)
    shape1.add(circle1)
    shape1.add(circle2)
    shape1.draw("blue")

    // 두번째 상자에 삼각형 하나, 원형 하나를 담고 빨간색으로 색을 칠한다.
    shape2.add(triangle3)
    shape2.add(circle3)
    shape2.draw("red")

    //상자두개를 큰 상자에 담고 모두 검정색으로 색을 칠한다.
    shape.add(shape1)
    shape.add(shape2)
    shape.draw("black")
}
  • CompositeDrawing 객체를 통해 Triangle 과 Circle 이라는 Leaf 객체들을 그룹으로 묶어서 한번에 동작 시켰다.
  • 또한, 사용되지는 않았지만 Square같은 다른 도형도 마찬가지로 Shape 인터페이스를 구현하고 있기 때문에 함께 취급 될 수 있다.

 

 

핵심

  • 트리 구조의 객체구조를 구현해야 할때 사용
    • 공통 인터페이스를 공유하는 두가지 기본 요소 유형인 leaf 와 composite 컨테이너를 제공한다.
    • 이를 통해 트리 구조와 유사하게 중첩된 재귀 객체 구조를 구성할 수 있다.
  • 클라이언트 코드가 단순 leaf 요소들과 composite 된 요소들을 모두 균일하게 처리하고 싶을때 사용
  • 기능이 너무 다른 클래스들에는 공통 인터페이스를 제공하기 어려우므로, 굳이 과도하게 일반화해서 이해하기 어렵게 만들 필요는 없음

 


간단히 hands on 을 통해 컴포짓 패턴을 살펴 보았다.

댓글