[question] Composite 패턴의 필요성 및 경험
Closed this issue · 4 comments
질문
12장에서 Strategy 패턴과 Composite 패턴에 대해 소개하고 있습니다. 둘 다 GoF 패턴에 포함돼있을 정도로 대중적인데요.
저는 전략 패턴은 추상화의 장점과 변경의 유연성 덕분에 많이 사용해봤으나, 컴포지트 패턴은 사용 경험이 비교적 적습니다. (사실 아예 없습니다 😭)
제가 이해한 것이 맞다면 Composite 패턴은 부모 클래스와 이를 상속받거나 구현하는 자식 클래스들이 여러 개 있을 때, 자식 클래스들을 효율적으로 관리하기 위한 패턴이라고 생각됩니다. 따라서 자식 클래스 개별의 로직을 한 집합(Composite)에서 처리하거나 관리할 때 유용하다고 생각이 들었습니다. 일급 컬렉션의 장점과 비슷하다고도 생각이 드네요.
그런데 Composite 패턴을 꼭 필요로 한 상황이 언제 올까 의문이 들었습니다.
인터페이스와 구현체를 통하여 각 객체들의 부가 사항을 충분히 구현할 수 있다고 생각이 들었고, 전략 패턴을 이용한 서비스 또는 자식 클래스들을 관리하는 상위 클래스를 하나 만들어줌으로써 잘 해결할 수 있지 않을까? 하는 생각이 들었어요.
여러분들이 Composite 패턴을 사용해본 경험을 듣고 싶어서 이슈를 남겨봅니다 :)
연관 챕터
[메모]
- 관련 이슈 : caffeine-library/random#15
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)
- 동일하게 다룬다 : 개별/복합 객체가 인터페이스로 드러난 행위를 동일하게 수행
- 여러 객체를 조직화하기 위해 재귀적 합성 기법을 사용하고 있음
사용 예제
요구사항
- 카테고리 테이블에서 이름이 변경되었을 때 해당 카테고리
코드 예제
- 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())
}
}