caffeine-library/Domain-Driven-Design

[question] Composite 패턴의 필요성 및 경험

Closed this issue · 4 comments

질문

12장에서 Strategy 패턴과 Composite 패턴에 대해 소개하고 있습니다. 둘 다 GoF 패턴에 포함돼있을 정도로 대중적인데요.
저는 전략 패턴은 추상화의 장점과 변경의 유연성 덕분에 많이 사용해봤으나, 컴포지트 패턴은 사용 경험이 비교적 적습니다. (사실 아예 없습니다 😭)

제가 이해한 것이 맞다면 Composite 패턴은 부모 클래스와 이를 상속받거나 구현하는 자식 클래스들이 여러 개 있을 때, 자식 클래스들을 효율적으로 관리하기 위한 패턴이라고 생각됩니다. 따라서 자식 클래스 개별의 로직을 한 집합(Composite)에서 처리하거나 관리할 때 유용하다고 생각이 들었습니다. 일급 컬렉션의 장점과 비슷하다고도 생각이 드네요.

그런데 Composite 패턴을 꼭 필요로 한 상황이 언제 올까 의문이 들었습니다.
인터페이스와 구현체를 통하여 각 객체들의 부가 사항을 충분히 구현할 수 있다고 생각이 들었고, 전략 패턴을 이용한 서비스 또는 자식 클래스들을 관리하는 상위 클래스를 하나 만들어줌으로써 잘 해결할 수 있지 않을까? 하는 생각이 들었어요.

여러분들이 Composite 패턴을 사용해본 경험을 듣고 싶어서 이슈를 남겨봅니다 :)

연관 챕터

#53

[메모]

오브젝트 10, 15장

ex. Movie 영화에서 할인 정책 도입
image

  • AmountPolicy, PercentPolicy 두 가지가 존재했어서 전략 패턴으로 이용했었음.
  • 두 개를 동시에 적용해야 되는 요구사항이 추가됨. 그 외에 두 개를 짬뽕 + 알파로 다루는 요구사항도 추가됨.
    • Composite 패턴을 이용하여 OverlappedPolicy 추가하여 여기서 순서 지정 및 정책 메서드 추가
      • 서비스에서 복잡한 로직 다룰 필요 없이, Policy로 OverlappedPolicy를 채택하면 됨.

Composite 패턴을 사용한 경험은 아니지만, Composite 패턴을 사용했으면 좋았을만한 코드를 가져와 보았습니다.

React 에서 어드민의 메뉴를 렌더링하는 코드인데, 이 코드를 짤 때는 Composite 패턴을 모르고 재귀로 작성하였는데 Menu-SubMenu 의 관계가 Composite 패턴에 적합해 보이네요.

subMenu 인지 판단하는 분기나 다른 분기들이 Composite 패턴을 사용하면 깔끔하게 제거되지 않을까 싶네요!

const renderMenus = useCallback(
    (menus: StaticMenuListType, isBookmark: boolean) => {
      return Object.entries(menus).map(([, menu]) => {
        const { url, title, icon, subMenus } = menu

        return subMenus ? (
          <SubMenu  // <-- 얘가 Composite 이 되겠죠?
            key={url}
            title={title}
            icon={icon}
            onTitleClick={(e) => {
              if (!collapsed) onToggleSubMenu(e.key)
            }}
            onTitleMouseEnter={(e) => {
              if (collapsed) {
                onOpenSubMenu(e.key)
              }
            }}
            onTitleMouseLeave={(e) => {
              if (collapsed) {
                onCloseSubMenu(e.key)
              }
            }}
          >
            {url === BOOKMARK_PREFIX
              ? renderMenus(bookmarkMenus, true)
              : renderMenus(subMenus, isBookmark)}
          </SubMenu>
        ) : (
          <MenuItem  // <-- 얘는 Leaf 노드!
            key={isBookmark ? BOOKMARK_PREFIX + url : url}
            icon={<Text>-</Text>}
            onMouseEnter={(e) => {
              if (collapsed) {
                onOpenSubMenu(e.key)
              }
            }}
            onMouseLeave={(e) => {
              if (collapsed) {
                onCloseSubMenu(e.key)
              }
            }}
          >
            <Link href={url}>{title}</Link>
          </MenuItem>
        )
      })
    },
    [openMenus]
  )

정의

Compose objects into tree structures to represent part-whole hierarchies. Composite lets
clients treat individual objects and compositions of objects uniformly.

  • 부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리 구조로 구성합니다. 사용자로 하여금 개별 객체와 복합 객체를 모두 동일하게 다룰 수 있도록 하는 패턴입니다. (GoF)
    • 동일하게 다룬다 : 개별/복합 객체가 인터페이스로 드러난 행위를 동일하게 수행
  • 여러 객체를 조직화하기 위해 재귀적 합성 기법을 사용하고 있음
스크린샷 2023-06-11 오후 2 06 00
사용 예제

요구사항

  • 카테고리 테이블에서 이름이 변경되었을 때 해당 카테고리

코드 예제

  • Component : 집합 관계에 포함시킬 수 있는 객체의 인터페이스
interface CategoryChangedSynchronizer {
    fun sync(nvMid: Long, category: Category)
}
  • Leaf : 자식이 없는 개체에 대하여 컴포지션의 기본 컴포넌트에 대한 동작을 정의
@Component
class KafkaCatalogCategoryChangedSynchronizer(
    private val kafkaTemplate: KafkaTemplate<MessageKey, Message>,
) : CategoryChangedSynchronizer {
    private val topic = "CUSTOM_CELINE_CDC_SS_PROD_CAT"
    override fun sync(nvMid: Long, category: Category) {
        val catNms = category.getCategoryNames () // ["가나다", "나다라", "다라마"]
        kafkaTemplate.send(
            topic,
            CustomProductCategoryChangedMessageKey(nvMid),
            CustomProductCategoryChangedMessage(
                category.catShapeTpCd,
                catNms.getOrNull(0),
                catNms.getOrNull(1),
                catNms.getOrNull(2),
                catNms.getOrNull(3)
            )
        )
    }
}
  • Composite : 자식이 있는 컴포넌트에 대한 동작을 정의하고 자식 컴포넌트를 갖고 있음
@Component
class CompositeCategoryChangedSynchronizer(
    private val synchronizer: List<CategoryChangedSynchronizer>
) : CategoryChangedSynchronizer {
    override fun sync(nvMid: Long, category: Category) {
        synchronizer.forEach {
            try {
                it.sync(nvMid, category)
            } catch (e: RuntimeException) {
                // TODO : 색인 실패 시 처리
            }
        }
    }
}
  • Client : Component 인터페이스를 통해 구성된 컴포넌트를 조작
@Component
class CategoryChangedEventHandler(
    private val categoryRepository: CategoryRepository,
    private val synchronizer: CategoryChangedSynchronizer // Composite 사용
) {
    fun handle(event: CategoryChangedEvent) {
        val category = categoryRepository.findById(event.nvMid.toString())
        if (!category.isPresent) return
        synchronizer.sync(event.nvMid, category.get())
    }
}