JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 컴포지트 패턴 (Composite)
1. 컴포지트 패턴이란? 🌳
개념: 객체들을 트리 구조로 구성하여 **부분-전체 계층(Part-Whole Hierarchy)**을 표현하는 패턴입니다. 이 패턴을 사용하면, 클라이언트가 **개별 객체(Leaf)**와 **복합 객체(Composite)**를 구분하지 않고 동일하게 다룰 수 있습니다.
가장 완벽한 예는 컴퓨터의 파일 시스템입니다.
- 파일(File): 개별 객체 (Leaf)
- 폴더(Folder): 다른 파일이나 폴더를 담는 복합 객체 (Composite)
우리는 파일의 이름을 바꾸거나 폴더의 이름을 바꿀 때 똑같이 '이름 바꾸기' 기능을 사용합니다. 파일의 크기를 보거나 폴더의 총 크기를 볼 때도 '크기 보기'라는 동일한 동작으로 처리합니다. 이처럼 컴포지트 패턴은 단일 객체와 집합 객체를 똑같이 취급할 수 있게 해주는 마법을 부립니다.
2. 왜 사용할까요?
- 통일된 인터페이스: 클라이언트 코드가 매우 단순해집니다. 객체가 단일 객체인지 복합 객체인지 신경 쓸 필요 없이, 공통 인터페이스의 메서드만 호출하면 됩니다.
- 계층 구조 표현: 조직도, GUI 컨테이너, 파일 시스템 등 부분과 전체의 관계를 가지는 계층 구조를 표현하는 데 매우 적합합니다.
- 유연한 구조: 새로운 종류의 개별 객체나 복합 객체를 쉽게 추가할 수 있습니다.
3. Java로 구현하는 방법
시나리오: 도형 그리기 프로그램을 만들어 보겠습니다. 점(Dot), 원(Circle) 같은 개별 도형도 그릴 수 있고, 여러 도형을 묶어서 하나의 그룹(CompoundGraphic)으로 만들어 한꺼번에 그리고 이동시킬 수 있어야 합니다.
1단계: 공통(Component) 인터페이스 정의
모든 개별 객체(Leaf)와 복합 객체(Composite)가 구현해야 할 공통 인터페이스입니다.
// 모든 도형 객체가 구현해야 할 공통 인터페이스
public interface Graphic {
void move(int x, int y); // 도형을 특정 위치로 이동
void draw(); // 도형을 그리기
}
2단계: 개별 객체(Leaf) 클래스 구현
계층 구조의 가장 말단에 위치하는, 더 이상 자식을 가지지 않는 기본 객체입니다.
// 개별 객체 1: 점
public class Dot implements Graphic {
protected int x, y;
public Dot(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public void move(int x, int y) {
this.x += x;
this.y += y;
}
@Override
public void draw() {
System.out.println(x + ", " + y + " 위치에 점을 그립니다.");
}
}
// 개별 객체 2: 원
public class Circle extends Dot {
private int radius;
public Circle(int x, int y, int radius) {
super(x, y);
this.radius = radius;
}
@Override
public void draw() {
System.out.println(x + ", " + y + " 위치에 반지름 " + radius + "짜리 원을 그립니다.");
}
}
3단계: 복합 객체(Composite) 클래스 구현
이 패턴의 핵심입니다. 복합 객체는 자식 Graphic 객체들을 담는 리스트를 가지고 있습니다. 그리고 move(), draw() 같은 메서드가 호출되면, 자신이 가진 자식들의 메서드를 순서대로 다시 호출해 줍니다.
import java.util.ArrayList;
import java.util.List;
public class CompoundGraphic implements Graphic {
// 자신에게 속한 자식 Graphic 객체들의 리스트
private List<Graphic> children = new ArrayList<>();
// 자식들을 추가하고 제거하는 메서드
public void add(Graphic child) {
children.add(child);
}
public void remove(Graphic child) {
children.remove(child);
}
// move, draw 같은 요청이 오면, 자식들에게 그대로 전달(위임)한다.
@Override
public void move(int x, int y) {
for (Graphic child : children) {
child.move(x, y);
}
}
@Override
public void draw() {
System.out.println("=== 복합 객체 그리기 시작 ===");
for (Graphic child : children) {
child.draw();
}
System.out.println("=== 복합 객체 그리기 완료 ===");
}
}
4단계: 클라이언트 코드에서 사용하기
클라이언트는 개별 도형이든, 도형 그룹이든 똑같은 draw() 메서드로 처리할 수 있습니다.
public class ImageEditor {
public static void main(String[] args) {
// 모든 도형을 담을 최상위 복합 객체
CompoundGraphic all = new CompoundGraphic();
// 개별 도형들을 생성
all.add(new Dot(1, 2));
all.add(new Circle(5, 3, 10));
// 다른 복합 객체를 생성
CompoundGraphic group = new CompoundGraphic();
group.add(new Dot(100, 200));
group.add(new Circle(500, 300, 50));
// 최상위 복합 객체에 다른 복합 객체를 추가
all.add(group);
// 모든 도형을 그리기
// 클라이언트는 all 내부에 어떤 구조로 도형이 있는지 전혀 신경쓸 필요가 없다.
all.draw();
System.out.println("\n=== 모든 객체를 10, 10 만큼 이동 ===");
all.move(10, 10);
all.draw();
}
}
4. 컴포지트 패턴과 실제 사례
- UI 툴킷: Java Swing/AWT, Android 등 거의 모든 GUI 툴킷에서 컴포지트 패턴을 사용합니다. Button, TextField 같은 개별 컴포넌트(Leaf)와, 이들을 담는 Panel, ViewGroup 같은 컨테이너(Composite)가 모두 Component라는 공통 상위 타입을 가집니다. 그래서 패널에 버튼을 추가하고 패널을 화면에 그리면, 패널 안의 버튼까지 알아서 그려집니다.
- 조직도: 사원(Leaf)과 팀장(Composite)이 모두 Employee라는 공통 인터페이스를 가질 수 있습니다. 팀장에게 '업무 보고'를 요청하면, 팀장은 자신의 업무를 보고하고 자신이 관리하는 팀원들에게도 '업무 보고'를 요청하는 식으로 동작할 수 있습니다.
5. 문제 제시 및 답변 💡
문제: 온라인 쇼핑몰의 상품 카테고리를 설계하려고 합니다. 카테고리는 다른 하위 카테고리를 포함할 수 있고, 가장 마지막 카테고리는 실제 상품들을 포함합니다. 예를 들어 '가전' > '주방가전' > '냉장고'와 같은 구조입니다.
이 시스템에서 '주방가전' 카테고리에 속한 모든 상품의 가격 총합을 구하는 기능을 컴포지트 패턴을 이용해 어떻게 설계할 수 있을까요?
답변:
컴포지트 패턴을 적용하기에 완벽한 시나리오입니다.
- CategoryComponent 인터페이스: 모든 카테고리와 상품이 구현해야 할 공통 인터페이스를 만듭니다. 이 인터페이스에는 getPrice() 메서드가 포함됩니다.
- Product 클래스 (Leaf): CategoryComponent를 구현하는 개별 상품 클래스입니다. getPrice()는 자신의 가격을 반환합니다.
- Category 클래스 (Composite): CategoryComponent를 구현하는 복합 객체입니다. 내부에 List<CategoryComponent> 타입의 자식 리스트를 가집니다. getPrice() 메서드는 자신이 가진 모든 자식들(Product 또는 다른 Category)의 getPrice() 결과를 합산하여 반환합니다.
이렇게 설계하면, 클라이언트는 '주방가전'이라는 Category 객체의 getPrice() 메서드 하나만 호출하면 됩니다. 그러면 '주방가전' Category는 내부적으로 '냉장고', '전자레인지' 등 하위 카테고리나 상품들의 getPrice()를 재귀적으로 호출하여 최종 가격 총합을 계산해 돌려줍니다.
'프로그래밍 > 알고리즘&자료구조&패턴' 카테고리의 다른 글
| 디자인패턴 - 옵저버 패턴 (2) | 2025.08.18 |
|---|---|
| 디자인패턴 - 전략패턴 (3) | 2025.08.18 |
| 디자인패턴 - 프록시 패턴 (1) | 2025.08.18 |
| 디자인패턴 - 퍼사드 패턴 (2) | 2025.08.18 |
| 디자인패턴 - 데코레이터 패턴 (1) | 2025.08.18 |