구조 패턴 - 복합체(Composite)
- 참조: GoF의 디자인 패턴
- Composite pattern 안전성: https://en.wikipedia.org/wiki/Composite_pattern#Java
- 복합체(Composite) 패턴
부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리구조를 형성한다. 개별객체와 복합객체를 동일하게 다룰 수 있도록 한다.
- 시나리오
PPT 문서를 작성하다보면 작은 요소들을(예를 들면 선, 텍스트, 도형들) 하나의 큰 요소로 만들고 난 후, 이를 수월하게 다루기 위해서 그룹화(Grouping)를 할때가 있다. 그룹화를 하면 한 번의 클릭으로 여러 요소들을 한꺼번에 선택하여 작업이 훨씬 수월하다.
작은 요소들을 클래스로 이 작은 요소들이 모인것을 컨테이너라고 할 때, 이를 코드로 구현하려면 해당 요소가 요소인지 컨테이너인지 판단하여 분기처리를 해야 한다.
- 문제 해결과 패턴의 적용
이 문제를 해결하기 위해 복합체 패턴에서는 작은 요소의 클래스들과 컨테이너를 모두 표현할 수 있는 하나의 추상화 클래스를 정의한다. PPT에서 Line, Rectangle, Text 요소들과 컨테이너인 Picture 요소가 있다고 가정할 때 복합체 패턴을 사용하면 아래와 같은 다이어그램으로 표현할 수 있다.
위의 다이어그램에서 Line, Rectangle, Text 는 기본 요소들이며 우리가 주목해야할것은 Picture 와 Graphic 이다.
Graphic은 추상 클래스로서 기본 요소들의 기본 기능인 Draw 기능을 가지고 있다. 또한 Grouping 기능에서 추가와 삭제기능에 필요한 Add과 Remove 메소드를 가지고 있다. 이런 추상적 Graphic 요소는 Picture 에서 graphics 집합(Aggregation)에 기본 요소들이나 다른 컨테이너를 포함할 수 있게 됨으로써 재귀적인 특성을 지니게 된다.
이 패턴의 참여자들은 아래와 같다.
- Component(Graphic): 집합 관계를 가질 모든 객체의 대한 인터페이스이다. 기본 요소들과 기본 요소들을 관리할 때 필요한 인터페이스를 정의한다.
- Leaf(Rectangle, Line, Text): 말단의 객체이며 Child 가 없는 기본 요소들이다.
- Composite(Picture): 자식이 있는 구성요소에 대해서 행동을 정의한다. 자식관련 연산을 구현하면서 복합하는 요소들을 저장한다.
- Client: Component 인터페이스를 통해 복합 구조내의 객체들을 조작한다.
- 안정성과 투명성
위의 다이어그램처럼 구현하면 Leaf 나 Composite 에 상관없이 일관된 Component 추상 인터페이스를 통해 다룰 수 있다는 장점이 있다. 하지만 기본적으로 상속관점에서보면 Component 가 Composite 의 연산까지 모두 가지고 있으므로 Leaf 는 필요 없는 연산도 기본적으로 구현해야 한다. 이런 문제 때문에 구현방법은 2 가지로 나뉜다.
투명성의 관점: 서브 클래스 모두에게 동일한 인터페이스를 유지시켜서 이를 사용하는 사용자에게 인터페이스의 투명성을 제공한다. 이에 대한 비용으로 사용자가 Leaf 에 없는 연산인 Add() 나 Remove() 에 대한 연산을 호출할 경우 이를 위한 방어로직을 추가해야 한다.
안전성의 관점: Add()나 Remove()를 Composite 에만 두면 Leaf 에는 필요한 연산만 있으므로 사용자는 Leaf 의 메소드를 호출할 때 안전성을 갖는다. 하지만 Leaf 와 Composite 가 서로 다른 인터페이스를 갖게 되어 동일한 대상으로 간주될 수 없다.
이런점 때문에 getComposite() 와 같은 연산을 추가하여 Leaf 에서는 null 반환을, Composite 에서는 자기자신을 반환하도록 구현한다. 이렇게 되면 null 체크를 통해 해당 객체가 Composite 인지 판단할 수 있게 된다.
- Java 예제
위의 예제를 구현해보자. 아래는 Component 참여자에 해당하는 Graphic 을 정의한것이다. 위에서 언급한 안전성의 관점보다 투명성의 관점을 택하였다. draw 외에 나머지 연산들은 Composite 에서만 의미가 있으므로 아무 행동을 하지 않거나 null 을 반환하는 default 메소드를 선언하였다.
public interface Graphic {
void draw();
default void add(Graphic graphic) {}
default Graphic getGraphic() {
return null;
}
default void remove(Graphic graphic) {}
}
Leaf 에 해당하는 참여자들은 특별할것 없이 draw 연산을 구현해주면 된다.
public class Line implements Graphic {
@Override
public void draw() {
System.out.println("Draw line");
}
}
public class Rectangle implements Graphic {
@Override
public void draw() {
System.out.println("Draw Rectangle");
}
}
public class Text implements Graphic {
@Override
public void draw() {
System.out.println("Draw text");
}
}
다음은 Composite 참여자에 해당하는 Picture 코드이다.
public class Picture implements Graphic {
private List<Graphic> graphics;
public Picture() {
super();
this.graphics = new ArrayList<Graphic>();
}
@Override
public void add(Graphic graphic) {
graphics.add(graphic);
}
@Override
public Graphic getGraphic() {
return this;
}
@Override
public void remove(Graphic graphic) {
graphics.remove(graphic);
}
@Override
public void draw() {
graphics.stream()
.forEach(Graphic::draw);
}
}
Test 코드를 작성해보자.
public static void main(String[] args) {
System.out.println("Phase 1 started..............");
Graphic line = new Line();
line.draw();
Graphic text = new Text();
text.draw();
Graphic rectangle = new Rectangle();
rectangle.draw();
Graphic picture = new Picture();
if(picture.getGraphic() != null) {
picture.add(line);
picture.add(text);
picture.add(rectangle);
}
System.out.println("Phase 1 picture draw..............");
picture.draw();
System.out.println("Phase 2 started..............");
Graphic text2 = new Text();
text2.draw();
Graphic parentPicture = new Picture();
if(parentPicture.getGraphic() != null) {
parentPicture.add(text2);
parentPicture.add(picture);
}
System.out.println("Phase 2 picture draw..............");
parentPicture.draw();
}
Leaf 와 Composite 요소 모두 Component 인 Graphic 인터페이스 하나로 다룰 수 있게 되었다. 이는 투명성을 우선하여 구현한 것인데, 안전성을 우선하여 구현한 version 은 wiki 를 참조하길 바란다.(https://en.wikipedia.org/wiki/Composite_pattern#Java)