부분과 전체의 일관된 처리
회사를 생각해 보세요. 직원과 부서가 있습니다. 이 두 가지는 본질적으로 다릅니다. 부서는 다른 부서나 직원을 포함할 수 있지만, 직원은 그럴 수 없습니다. 하지만 ‘회사’라는 개념으로 봤을 때는 부서나 직원이나 회사의 일부분으로 인식합니다. 부서를 해체하면 그 부서에 해당하는 요소가 해체되고, 해당 부서의 직원 또한 해고될 수 있습니다. 개별 직원을 해고하면 해당 직원만 해고됩니다. 부서나 직원은 본질적으로 다르지만, 해체나 해고는 행위에서는 같습니다.
컴포지트 패턴은 개별 객체와 객체들의 집합을 동일한 방식으로 다룰 수 있게 해주는 구조적 디자인 패턴입니다.
1980년대 Smalltalk 환경에서 개발된 MVC(Model-View-Controller) 아키텍처에서 이미 View 객체들이 컴포지트 패턴과 유사한 방식으로 구성되었습니다. 최초의 상업적 활용 중 하나는 1980년대 후반 애플의 MacApp 프레임워크와 NeXT의 Interface Builder에서 찾아볼 수 있습니다. 이들 시스템에서는 복합 UI 요소와 단일 UI 요소가 동일한 인터페이스를 통해 처리되었습니다.
컴포지트 패턴도 역시 1994년 GoF에 의해 공식적으로 문서화되었습니다.
컴포지트 패턴의 구조는 컴포넌트(Component), 리프(Leaf), 컴포지트(Composite) 세 가지 주요 구성 요소로 되어 있습니다. 컴포넌트는 모든 객체에 대한 공통 인터페이스(회사)이고, 리프는 개별 객체(직원), 컴포지트는 자식을 가질 수 있는 복합 객체(부서)에 해당합니다.
// 컴포넌트: 모든 객체의 공통 인터페이스 (회사)
interface Employee {
void showDetails(); // 정보 표시
int getSalary(); // 급여 계산
}
// 리프: 일반 직원(하위 직원이 없음)
class Developer implements Employee {
private String name;
private int salary;
public Developer(String name, int salary) {
this.name = name;
this.salary = salary;
}
@Override
public void showDetails() {
System.out.println("개발자: " + name + ", 급여: " + salary);
}
@Override
public int getSalary() {
return salary;
}
}
// 리프: 또 다른 유형의 일반 직원
class Designer implements Employee {
private String name;
private int salary;
public Designer(String name, int salary) {
this.name = name;
this.salary = salary;
}
@Override
public void showDetails() {
System.out.println("디자이너: " + name + ", 급여: " + salary);
}
@Override
public int getSalary() {
return salary;
}
}
// 컴포지트: 관리자/부서 (하위 직원을 가질 수 있음)
class Manager implements Employee {
private String name;
private int salary;
private List<Employee> subordinates = new ArrayList<>();
public Manager(String name, int salary) {
this.name = name;
this.salary = salary;
}
// 부하 직원 추가
public void addEmployee(Employee employee) {
subordinates.add(employee);
}
// 부하 직원 제거
public void removeEmployee(Employee employee) {
subordinates.remove(employee);
}
@Override
public void showDetails() {
System.out.println("관리자: " + name + ", 급여: " + salary);
System.out.println("팀 구성원:");
for (Employee employee : subordinates) {
employee.showDetails();
}
}
@Override
public int getSalary() {
// 관리자 자신의 급여 + 모든 부하 직원 급여의 합계
int totalSalary = salary;
for (Employee employee : subordinates) {
totalSalary += employee.getSalary();
}
return totalSalary;
}
}
위의 코드와 같이 회사(컴포넌트), 직원(리프), 부서(컴포지트)를 정의하였다면 이를 사용하는 방법은 아래와 코드와 같습니다.
public class CompanyStructure {
public static void main(String[] args) {
// 일반 개발자 생성
Employee dev1 = new Developer("김개발", 3000000);
Employee dev2 = new Developer("박코딩", 2800000);
// 디자이너 생성
Employee designer = new Designer("이디자인", 2700000);
// 개발팀 관리자 생성
Manager devManager = new Manager("최팀장", 5000000);
devManager.addEmployee(dev1);
devManager.addEmployee(dev2);
// 더 상위 관리자(부서장) 생성
Manager director = new Manager("강부장", 7000000);
director.addEmployee(devManager);
director.addEmployee(designer);
// 개별 직원 정보 출력
System.out.println("=== 개별 직원 정보 ===");
dev1.showDetails();
// 팀 전체 정보 출력
System.out.println("\n=== 개발팀 정보 ===");
devManager.showDetails();
// 부서 전체 정보 출력
System.out.println("\n=== 부서 전체 정보 ===");
director.showDetails();
// 부서 전체 급여 총액 계산
System.out.println("\n부서 전체 급여 총액: " + director.getSalary());
}
}
이를 실행하면 아래와 같이 결과가 출력됩니다.
=== 개별 직원 정보 ===
개발자: 김개발, 급여: 3000000
=== 개발팀 정보 ===
관리자: 최팀장, 급여: 5000000
팀 구성원:
개발자: 김개발, 급여: 3000000
개발자: 박코딩, 급여: 2800000
=== 부서 전체 정보 ===
관리자: 강부장, 급여: 7000000
팀 구성원:
관리자: 최팀장, 급여: 5000000
팀 구성원:
개발자: 김개발, 급여: 3000000
개발자: 박코딩, 급여: 2800000
디자이너: 이디자인, 급여: 2700000
부서 전체 급여 총액: 20500000
코드들을 정리해 보면, 모든 구성원(개발자, 디자이너, 관리자)이 동일한 Employee 인터페이스로 구현되어 통일되어 있습니다. 개별 직원이든 부서든 동일한 메서드(showDetails(), getSalary())를 호출함으로써 일관되게 사용합니다.
이처럼 컴포지트 패턴은 ‘전체-부분’ 관계를 갖는 계층적 구조를 표현하고, 개별 객체와 복합 객체를 동일하게 다룰 수 있게 해주는 패턴입니다.
컴포지트(Composite)는 ‘복합재, 합성, 결합’이라는 의미로 개별 객체인 리프들을 결합, 합성합니다. 본질적으로 같은 객체들을 묶어 하나의 복합 객체를 만들어 일관된 처리가 가능하게 합니다. 이 패턴을 통해 우리는 복잡한 트리 구조를 단순하고 일관된 방식으로 다룰 수 있습니다. 자연계의 나무와 마찬가지로, 컴포지트 패턴은 작은 부분들이 모여 더 큰 전체를 형성하는 유기적인 구조를 코드로 표현할 수 있게 해 줍니다.
복잡하면 복잡할수록 단순화하는 작업이 필요합니다. 일관된 행위와 속성을 묶어 하나로 만드는 과정을 통해 일관성과 각각의 다양성의 균형을 찾아야 합니다. 합성과 결합이 가능하려면 전혀 다른 것을 합성하는 것이 아닌 다름 속에 공통된 것을 찾아 결합하고 합성해야 조화를 이룰 수 있습니다.