본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 14. 컴파운드 패턴


지난 포스팅에선 객체에 대한 접근을 제어하는 프록시 패턴에 대해 알아보았다.


이번에는 여러 패턴을 섞어서 강력한 객체지향 디자인을 만드는 컴파운드 패턴에 대해 복습한다.


컴파운드 패턴

두 개 이상의 패턴을 결합하여 일반적으로 자주 등장하는 문제들에 대한 해법을 제공한다.


컴파운드 패턴은 딱히 정해진 의미, 단점, 장점이 뚜렷하지 않다.

패턴을 잘 활용하기 위해 서로 다른 패턴을 섞어 쓰는 디자인 방법이기 떄문이다.


다만 주의할 점은 패턴 몇 개를 결합해서 쓴다고 해서 무조건 컴파운드 패턴이 되는 것은 아니다.

컴파운드 패턴이라고 불릴 수 있으려면 여러 가지 문제를 해결하기 위한 용도로 쓰일 수 있는 일반적인 해결책이어야 한다.


웹에서 벌어지는 다양한 요청과 이에 대응하는 로직을 처리하고 처리 결과를 브라우저로 보여주기 위한 MVC패턴이 대표적이다.


가장 먼저 책에서 첫번째로 등장한 예제를 살펴보자.

오리들은 꽥꽥거리는 행동이 들어있다.


1. 기본 디자인

1-1. 모든 Duck 객체에서 구현해야 하는 인터페이스

public interface Quackable {

public void Quack();

}


1-2. Quackable을 구현한 Duck 클래스

public class MallardDuck implements Quackable {

public void quack() {

System.out.println("Quack");

}

}


1-3. 완전한 오리는 아니지만 오리의 행동을 할 수 있는 오리 호출기, 고무 오리 등

public class DuckCall implements Quackable {

public void quack() {

System.out.println("kwak");

}

}

public calss RubberDuck implements Quackable {

public void quack() {

System.out.println("Squack");

}

}


1번의 예시는 패턴을 적용하지 않은 기본적인 디자인이다.

오리와 거위는 같은 가금류들은 서로 잘 몰려다니므로 거위를 추가하고 싶다.

위의 상황에서 Quackable은 오리만을 위한 인터페이스이므로 서로 다른 인터페이스간의 호환이 필요하다.

거위를 마치 오리처럼 다룰 수 있도록 하기 위해 어댑터 패턴을 이용해보자.

자바를 이용하며 거위만 오리로 변환하면 되기 때문에 객체 어댑터를 적용한다.


2. 어댑터 패턴

2-1. 거위 클래스 정의

public class Goose {

public void honk() {

System.out,.println("Honk");

}

}


2-2. 거위용 어댑터 구현

public class GooseAdapter implements Quackable { // 타겟 = Quackable

Goose goose // 어댑티


public GooseAdapter(Goose goose) {

this.goose = goose;

}


public void quack() {

goose.honk();

}

}


변환결과가 될 타겟 인터페이스는 Quackable 변환되는 어댑티는 Goose이다.

Quackable과 오리에는 quack()가 있지만 거위는 honk()를 사용해야 한다.

클라이언트는 Goose 객체를 만들고 Quackable을 구현하는 인터페이스로 감싸기만 하면 된다.


이번에는 오리의 quack()메소드가 호출 될 때 행동을 추가하고 싶다.

오리떼가 있을 때, 그 오리떼가 낸 꽥소리의 총 회수를 세어서 연구하고 싶은데,

오리 클래스는 그대로 두면서 행동을 확장해야 한다.

이번엔 데코레이터 패턴을 이용해서 행동을 확장시켜보자.


3. 데코레이터 패턴

3-1. 데코레이터 클래스 정의

// 데코레이터 = QuackCounter

public class QuackCounter implments Quackable { // 컴포넌트 = Quackable

Quackable duck; // 구상 컴포넌트

static int numberOfQuacks; // 오리떼의 메소드 호출회수


public QuackCounter(Quackable duck) {

this.duck = duck;

}


public void quack() {

duck.quack();

numberOfQuacks++;

}


public static int getQuacks() {

return numberOfQuacks;

}

}


어댑터를 만들 때와 마찬가지로 컴포넌트(타겟 인터페이스)인 Quackable을 구현해야 한다.

기존 기능을 호출하기 위해 구상 컴포넌트를 인스턴스 변수로 갖고,

오리떼의 quck() 호출 회수를 세어야 하기 때문에 변수 공유를 위해 클래스 변수를 사용한다.


* Goose는 Quackable을 구현하지 않기 때문에 데코레이터로 감쌀 수 없다.


위의 디자인 만으로는 꽥소리를 내는 회수를 제대로 세지 못하는 경우가 있다.

회수를 세야하는 오리인데 실수로 QuackCounter로 감싸지 않는다던가 하는 경우 떄문에 말이다.

데코레이터를 쓸 때는 객체들을 제대로 포장하지 않으면 원하는 행동을 추가할 수 없다.


오리 객체를 생성하는 작업을 한 군데에서 몰아서 담당하기 위해 팩토리 패턴을 이용해보자.

오리를 생성하고 데코레이터로 감싸는 부분을 따로 빼내서 캡슐화하는 것이다.


4. 팩토리 패턴

4-1. 오리를 생산하기 위한 추상 팩토리 클래스 정의

public abstract class AbstractDuckFactory {


public abstract Quackable createMallardDuck();

public abstract Quackable createDuckCall();

public abstract Quackable createRubberDuck();

}


4-2. 데코레이터가 없는 오리를 만드는 팩토리 정의

public class DuckFactory extends AbstractDuckFactory {


public Quackable createMallardDuck() {

return new MallardDuck();

}


public Quackable createDuckCall() {

return new DuckCall();

}


public Quackable createRubberDuck() {

return new RubberDuck();

}

}


4-3. 꽥소리를 세는 기능인 데코레이터를 포함한 팩토리 정의

public class CountingDuckFactory extends AbstractDuckFactory {


public Quackable createMallardDuck() {

return new QuackCounter(new MallardDuck());

}


public Quackable createDuckCall() {

return new QuackCounter(new DuckCall());

}


public Quackable createRubberDuck() {

return new QuackCounter(new RubberDuck());

}

}


모든 메소드에서 Quackable객체를 꽥꽥거린 회수를 세기 위한 데코레이터로 감싼 뒤 반환한다.

클라이언트는 Quackable 타입인 것만 알지 조금 다른 객체가 리턴되었다는 것을 전혀 알 수 없다.

하지만 실제 인스턴스는 데코레이터로 감싼 오리 객체이기 때문에 꽥꽥거린 회수를 전부 셀 수 있다.


오리가 점점 많아져 오리 객체를 종류별로 분류한다든가 하기 위해 오리떼를 일괄적으로 관리하고 싶다.

오리들로 구성된 컬렉션, 또는 그 컬렉션의 부분 컬렉션을 다룰 수 있는 방법이 필요하다.

여러 오리들에 대한 정보를 한번에 출력한다든가 하는 작업을 적용할 수 있으면 더 좋다.


이번엔 부분과 전체를 다루기 위해 컴포지트 패턴을, 컬렉션의 반복 작업을 처리하기 위해 이터레이터 패턴을 적용해보자.

5. 이터레이터와 컴포지트 패턴

5-1. 컴포지트와 잎에서 구현해야할 인터페이스 정의

// 복합 객체 = Flock

public class Flock implments Quackable { // 잎 = Quackable

ArrayList<Quackable> quackers = new ArrayList<>();


public void add(Quackable quacker) {

quackers.add(quacker);

}


public void quack() {

Iterator<Quackable> iter = quackers.iterator();

while(iter.hasNext()) {

Quackable quackable = iter.next();

quacker.quack();

}

}

}


자식을 갖는 복합 객체는 Flock, 자식이 없는 잎 객체는 Quackable이다.

Flock도 결국은 Quackable이므로 Flock안에 있는 모든 오리들을 챙겨야 한다.

ArrayList에 대해서 순환문을 돌리면서 재귀적으로 각 원소의 quack()를 호출한다.


* 여기서는 복합 객체에서 자식을 관리하기 위한 메소드를 복합 객체에만 집어넣었다.

  이렇게 해서 투명성이 떨어지는 대신 안정성이 높아진다. 잎 객체에 add와 같은 메소드가 들어있지 않기 때문이다.


지금까지... 어댑터를 이용해 거위를 오리처럼 다루는게 가능해졌고,

데코레이터를 이용해 오리가 꽥꽥 거리는 회수를 세는게 가능해졌고,

팩토리를 이용해 데코레이터의 기능을 확실하게 사용하기 위해 오리를 포장하는게 가능해졌고,

이터레이터와 컴포지트를 이용해 오리떼를 관리하는게 가능해졌다.


이번엔 마지막으로 오리들을 각각 하나씩 관리하는 기능까지 추가해보고 싶다.

꽥꽥거리는 오리들을 하나씩 실시간으로 추적하기 위해 옵저버 패턴을 적용해보자.


6. 옵저버 패턴

6-1. 관찰 대상이자 연락을 돌리는 주제를 의미하는 Observable 인터페이스 정의

public interface QuackObservable {

public void registerObserver(Observer observer); // 옵저버 등록

public void notifyObservers(); // 연락 송신

}


6-2. 모든 Quackable에서 관찰 대상인 QuackObservable을 구현

public interface Quackable extends QuackObservable {

public void quack();

}


6-3. Quackable을 구현하는 모든 구상클래스에서 주제가 되기 위해 QuackObservable을 구현

* 이번에는 모든 구상 클래스에서 일일이 등록 및 연락용 메소드를 구현하지 않고

  한 보조 클래스에 캡슐화해 놓은 다음 구성을 통해서 구상 클래스에 포함시켜서 구현해본다.

  이렇게 하면 실제 코드는 한 군데에만 작성해 놓고, 필요한 작업을 해당 클래스에 위임하면 된다.


public class Observable implements QuackObservable {

ArrayList<Observer> observers = new Observer();

QuackObservable duck;


public Observable(QuackObservable duck) { // Quack오리에 옵저버를 적용한 QuackObservable

this.duck = duck;

}


public void registerObserver(Observer observer) {

observers.add(observer);

}

public void notifyObservers() {

Iterator<Observer> iter = observers.iterator();

while(iter.hasNext()) {

Observer observer = iter.next();

observer.update(duck);

}

}

}


* 모든 구상클래스는 이 Quackable을 구성요소로 포함해야 한다.


6-4. Observer 보조 객체와 Quackable 구상 클래스를 결합한다.

public class MallardDuck implments Quackable {

Observable observable;


public MallardDuck() {

observable = new Observable(this); // Quackable은 주제인 QuackObservable을 구현

}


public void quack() {

System.out.println("Quack");

notifyObservers();

}


// 요청을 보조 클래스인 Observable에게 위임

public void registerObserver(Observer observer) {

observable.registerObserver(observer);

}


public void notifyObservers() {

observable.notifyObservers();

}

}


6-5. 마지막으로 옵저버 패턴의 옵저버 쪽을 완성

public interface Observer {

public void update(QuackObservable duck);

}


public class Quackologist implements Observer {

public void update(QUackObservable duck) {

System.out.println("Quackologist : " + duck + " just quacked.");

}

}


주제 객체로부터 quack() 호출이 된 객체의 정보와 함께 연락이 오면

해당 정보를 출력하는 update() 메소드만 구현하면 된다.


지금까지 오리 시뮬레이터의 기능을 구현하기 위해 이용한 패턴 적용 작업들은 다음과 같다.

(1) 갑자기 거위가 등장해서 자기도 Quackable이 되고 싶다고 요청

- 어댑터 패턴을 이용해 거위를 Quackable에 맞게 구현.

  이제 거위는 어댑터로 감싸져 있기 때문에 거위 객체의 quack()를 호출하면 자동으로 honk()가 호출된다.


(2) 꽥학자들이 꽥소리 회수를 세고싶다고 요청

- 데코레이터 패턴을 이용해서 QuackCounter 데코레이터를 추가.

  quack() 메소드 호출 자체는 그 데코레이터로 싸여져 있는 Quackable에 의해 처리되고 quack()의 호출된 회수를 셀 수 있게 되었다. 


(3) 하지만 QuackCounter로 장식되지 않은 Quackble 객체가 있을 수도 있음

- 추상 팩토리 패턴을 적용해서 객체를 만들도록 구성.

  오리 객체를 만들 때는 항상 팩토리에 요청을 하게 될 테고, 팩토리는 데코레이터로 싸여져 있는 오리를 반환하게 되었다.


(4) 모든 오리와 거위, Quackable 객체들을 관리하는 게 힘들어지기 시작

- 컴포지트 패턴을 적용해서 오리들을 모아서 오리떼 단위로 관리.

  꽥학자들은 수많은 오리들을 부분별로 관리할 수도 있으며 이터레이터를 이용해 반복작업까지 처리할 수 있게 되었다.


(5) Quackable에서 꽥소리를 냈을 때 그런 일이 있다는 것을 연락 받고 싶어함

- 옵저버 패턴을 적용해서 Quackologist를 Quackable의 옵저버로 등록.

  Quackable에서 꽥소리를 낼 때마다 연락을 받을 수 있게 되었으며 주제에서 연락을 돌릴 때 이터레이터 패턴을 적용했다.


의문점

(1) 여기서 쓰인 게 정말 컴파운드 패턴인가?

- 그냥 여러가지 패턴을 섞어서 썻을 뿐, 컴파운드 패턴이라고 할 순 없다.

  컴파운드 패턴이라고 할 수 있으려면 몇 개의 패턴을 복합적으로 사용해서

  일반적인 문제를 해결할 수 있는 것이어야 한다.


(2) 어떤 문제가 닥쳤을 때 여러 디자인 패턴을 적용하다 보면 해결이 가능한가?

- 앞에서 여러 가지 패턴을 사용한 이유는 그냥 여러 패턴을 함께 사용할 수 있다는 것을 보여준 예시일 뿐이다.

  실전에서는 이런 식으로 디자인을 하는 일은 절대 없을 것이다.

  상황에 따른 올바른 객체지향 디자인 원칙을 적용하는 것만으로도 문제가 해결되기도 한다.

  중요한 것은 패턴은 반드시 상황에 맞게 써야 한다는 것이다.


여러 패턴을 섞어서 강력한 객체지향 디자인을 만드는 컴파운드 패턴에 대한 복습을 마치겠다.

이제 매우 강력한 컴파운드 패턴인 MVC패턴만이 남았다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴

8. 퍼사드 패턴

9. 템플릿 메소드 패턴

10. 이터레이터 패턴

11. 컴포지트 패턴

12. 스테이트 패턴

13. 프록시 패턴

14. 컴파운드 패턴