👨🏼💻 컴포지트 패턴은 컴포지트(복합 객체)와 단일 객체를 동일한 컴포넌트로
취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록
하는 구조 패턴이다.
이 패턴은 전체-부분의 관계를 가지는 객체들 사이의 관계를 트리 계층 구조로
정의해야 할 때 유용하다. 파일 시스템 구조를 떠올려보면 쉽게 이해할 수 있다.
폴더(디렉터리) 내에는 파일이 들어있거나, 파일을 포함한 또 다른 폴더가 들어있을 수 있다. 이처럼 복합적인 형태를 가리켜 Composite 객체라 부른다. 반면 파일은 단일
객체이기 때문에 Leaf 객체라 불린다.
컴포지트 패턴은 이런 폴더와 파일을 동일 타입으로 취급하여 구현을 단순화 시키는 것이 목적이다.
계층 구조를 구현하다 보면 복잡해질 수 도 있는 복합 객체를 재귀 동작을 통해 하위
객체들에게 작업을 위임하는 형태로 구현한다. 그러면 복합 / 단일 객체를 대상으로
동일 작업을 적용할 수 있어 두 객체를 구분할 필요가 줄어든다.
정리하자면, 컴포지트 패턴은 그릇과 내용물을 동일시해서 재귀적 구조를 만들기 위한 디자인 패턴이라 말할 수 있다.
이 패턴의 핵심은 Composite와 Leaf가 동시에 구현하는 operation()
인터페이스
추상 메서드를 정의하고, Composite 객체의 operation()
메서드는 스스로를
호출하는 재귀 형태로 구현하는 것이다. 파일 시스템같은 트리 구조를 생각해보면,
재귀적으로 반복되는 형식이 나타난다.
따라서 단일체와 복합체를 동일 개체로 취급하여 처리하기 위해 재귀 함수 원리를
이용한다.
각종 물품을 담는 박스가 존재할 때 객체간 관계가 다음과 같은 구조로 형상화될 수
있다.
Box
public class Box {
private List<Trousers> trousers = new ArrayList<>();
private List<Socks> socks = new ArrayList<>();
public int price() {
int tPrice = trousers.stream()
.mapToInt(Trousers::price)
.sum();
int sPrice = socks.stream()
.mapToInt(Socks::price)
.sum();
return tPrice + sPrice;
}
public void addSocks(Socks s) {
socks.add(s);
}
public void addTrousers(Trousers t) {
trousers.add(t);
}
}
Socks
public class Socks {
private int weight;
public Socks(int weight) {
this.weight = weight;
}
public int price() {
return this.weight / 100 * 200;
}
}
Trousers
public class Trousers {
private int weight;
public Trousers(int weight) {
this.weight = weight;
}
public int price() {
return this.weight / 100 * 200;
}
}
다음과 같이 택배 아이템이 추가되는 상황이 마주한다면?
public class Box {
private List<Trousers> trousers = new ArrayList<>();
private List<Socks> socks = new ArrayList<>();
private List<Golds> golds = new ArrayList<>();
public int price() {
int tPrice = trousers.stream()
.mapToInt(Trousers::price)
.sum();
int sPrice = socks.stream()
.mapToInt(Socks::price)
.sum();
int gPrice = golds.stream()
.mapToInt(Golds::price)
.sum();
return tPrice + sPrice + gPrice;
}
public void addSocks(Socks s) {
socks.add(s);
}
public void addTrousers(Trousers t) {
trousers.add(t);
}
public void addGolds(Golds g) {
golds.add(g);
}
}
기존 Box
에 추가된 아이템 타입의 필드를 추가하고, 가격을 계산하는 로직도
수정해야 한다. 따라서 OCP를 위배하게 된다.
한편, 만약 박스 내에 박스가 포함될 수 있는 형태도 가능하다면 로직을 어떻게
변경해야 할까?
Box
public class Box {
private List<Trousers> trousers = new ArrayList<>();
private List<Socks> socks = new ArrayList<>();
private List<Golds> golds = new ArrayList<>();
private List<Box> boxes = new ArrayList<>();
public int price() {
int tPrice = trousers.stream()
.mapToInt(Trousers::price)
.sum();
int sPrice = socks.stream()
.mapToInt(Socks::price)
.sum();
int gPrice = golds.stream()
.mapToInt(Golds::price)
.sum();
int bPrice = boxes.stream()
.mapToInt(Box::price)
.sum();
return tPrice + sPrice + gPrice + bPrice;
}
public void addSocks(Socks s) {
socks.add(s);
}
public void addTrousers(Trousers t) {
trousers.add(t);
}
public void addGolds(Golds g) {
golds.add(g);
}
public void addBox(Box b) {
boxes.add(b);
}
}
Client
public class Client {
public static void main(String[] args) {
Box box = new Box();
Socks s1 = new Socks(100);
Socks s2 = new Socks(200);
Trousers t1 = new Trousers(600);
box.addSocks(s1);
box.addSocks(s2);
box.addTrousers(t1);
System.out.println(box.price());
Box box2 = new Box();
Golds g1 = new Golds(800);
box2.addBox(box);
box2.addGolds(g1);
System.out.println(box2.price());
}
}
이 경우에도 OCP를 위배하게 된다.
단순히 Object
를 이용해 포괄적으로 필드를 구성하고 instanceof
연산자로
객체를 구분하여 가격을 계산하는 방법을 생각할 수 있지만 그 경우에도 새로운 물건
추가시 분기문이 추가되기 때문에 OCP에 위배된다.
따라서 다음과 같은 사실을 포함하는 상위 개념은 ParceItem
을 설정할 수 있다.
Box
는 택배로 보낼 수 있는 항목을 여러 개 가질 수 있다.ParceItem
public abstract class ParceItem {
protected int weight;
public ParceItem(int weight) {
this.weight = weight;
}
public abstract int price();
}
Box
public class Box extends ParceItem {
private List<ParceItem> items = new ArrayList<>();
public Box(int weight) {
super(weight);
}
@Override
public int price() {
return items.stream()
.mapToInt(ParceItem::price)
.sum();
}
public void addItem(ParceItem item) {
items.add(item);
}
}
Socks
public class Socks extends ParceItem {
public Socks(int weight) {
super(weight);
}
@Override
public int price() {
return weight / 100 * 200;
}
}