지난 포스팅에선 객체에 대한 접근을 제어하는 프록시 패턴에 대해 알아보았다. 이번에는 여러 패턴을 섞어서 강력한 객체지향 디자인을 만드는 컴파운드 패턴에 대해 복습한다.

정의

컴파운드 패턴은 두 개 이상의 패턴을 결합하여 일반적으로 자주 등장하는 문제들에 대한 해법을 제공한다. 컴파운드 패턴은 딱히 정해진 의미, 단점, 장점이 뚜렷하지 않다. 패턴을 잘 활용하기 위해 서로 다른 패턴을 섞어 쓰는 디자인 방법이기 떄문이다.

다만 주의할 점은 패턴 몇 개를 결합해서 쓴다고 해서 무조건 컴파운드 패턴이 되는 것은 아니다. 컴파운드 패턴이라고 불릴 수 있으려면 여러 가지 문제를 해결하기 위한 용도로 쓰일 수 있는 일반적인 해결책이어야 한다. 웹에서 벌어지는 다양한 요청과 이에 대응하는 로직을 처리하고 처리 결과를 브라우저로 보여주기 위한 MVC패턴이 대표적이다.

오리 프로그램

다양한 오리들이 존재하는 프로그램에 대해 살펴보자. 처음에는 오리만 존재한다. 이후 요청사항이 계속 추가되면서 거위를 오리처럼 취급해야하고, 오리들이 꽥소리를 내는 횟수를 세야하고, 오리떼를 관리해야하고, 꽥소리에 대한 알림을 받아야 하는 등의 문제에 패턴을 적용하면서 해결해나간다.

  • 모든 Duck 객체에서 구현해야 하는 인터페이스
public interface Quackable {
	public void Quack();
}
  • Quackable을 구현한 Duck 클래스
public class MallardDuck implements Quackable {
    public void quack() {
        System.out.println("Quack");
    }
}
  • 완전한 오리는 아니지만 오리의 행동을 할 수 있는 오리 호출기, 고무 오리 등
public class DuckCall implements Quackable {
    public void quack() {
        System.out.println("kwak");
    }
}

public calss RubberDuck implements Quackable {
    public void quack() {
        System.out.println("Squack");
    }
}

첫번째 예시는 패턴을 적용하지 않은 기본적인 디자인이다. 오리와 거위와 같은 가금류들은 서로 잘 몰려다닌다. 오리에게 친구를 만들어주기 위해 거위를 추가하려고한다. 그러나, 현재 Quackable은 오리만을 위한 인터페이스이므로 서로 다른 인터페이스간의 호환이 필요하다.

어댑터 패턴을 적용한 오리같은 거위

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

  • 거위 클래스
public class Goose {
    public void honk() {
        System.out,.println("Honk");
    }
}
  • 거위용 어댑터
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() 메소드가 호출 되는 횟수를 세어야한다.

데코레이터 패턴을 적용한 꽥소리 카운터

오리 클래스는 그대로 두면서 행동을 확장하기 위해 데코레이터 패턴을 적용해보자.

  • 데코레이터
// 데코레이터 = 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을 구현해야 한다. 또한, 확장의 대상이 되는 quack() 메소드를 호출하기 위해 구상 컴포넌트와의 의존성을 설정한다. 거위는 Quackable을 구현하지 않기 때문에 데코레이터로 감쌀 수 없다.

꽥학자들에게 꽥소리카운터를 전달했더니 이 디자인은 모든 오리를 일일이 카운터로 감싸야만 정확한 카운터를 셀 수 있다며 항의한다. 만약 실수로 데코레이터로 오리를 감싸지 않는다면, 해당 오리의 카운터는 세어지지 않는다. 이처럼 데코레이터를 사용할 때는 객체들을 제대로 포장하지 않으면 의도치 않은 동작을 수행한다.

팩토리 패턴을 적용하여 꽥학자를 안심시키자

꽥학자들의 의견을 고려하여 오리 객체를 생성하는 작업을 한 군데에서 몰아서 담당하기 위해 팩토리 패턴을 적용해보자. 오리를 생성하고 데코레이터로 감싸는 부분을 따로 빼내서 캡슐화한다.

  • 추상 팩토리
public abstract class AbstractDuckFactory {
    public abstract Quackable createMallardDuck();
    public abstract Quackable createDuckCall();
    public abstract Quackable createRubberDuck();
}
  • 카운터 데코레이터로 감싸지 않은 오리를 만드는 팩토리
public class DuckFactory extends AbstractDuckFactory {
    public Quackable createMallardDuck() {
        return new MallardDuck();
    }

    public Quackable createDuckCall() {
        return new DuckCall();
    }

    public Quackable createRubberDuck() {
        return new RubberDuck();
    }
}
  • 카운터 데코레이터로 감싼 오리를 만드는 팩토리
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());
    }
}

꽥학자에게 제공되는 팩토리는 CountingDuckFactory 클래스이다. 해당 팩토리는 모든 메소드에서 오리를 카운터 데코레이터로 감싼 뒤 반환한다. 꽥학자는 반환되는 오리가 데코레이터를 포함하는 오리라는 것을 알 수 없지만, 실제 인스턴스는 카운터 데코레이터로 감싼 오리이기 때문에 꽥꽥거린 회수를 전부 셀 수 있다.

점점 오리들의 세계가 커지고 있다. 시간이 지날수록 오리의 종류가 많아지는 것을 고려하여 오리 객체를 종류별로 분류 하기 위해 오리떼를 일괄적으로 관리하려고한다. 오리들로 구성된 컬렉션, 또는 그 컬렉션의 부분 컬렉션을 다룰 수 있는 방법이 필요하다. 여러 오리들에 대한 정보를 한번에 출력한다든가 하는 작업을 적용할 수 있으면 더 좋다.

이터레이터와 컴포지트 패턴을 적용한 오리떼

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

  • 컴포지트 인터페이스
// 복합 객체 = Flock
public class Flock implments 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은 오리떼를 관리하는 컬렉션 역할을 한다. quack() 메소드를 호출하면 재귀적으로 자식요소의 quack() 메소드를 호출한다. Flock에 오리들을 추가하여 quack()을 호출하면 해당 컬렉션에 들어있는 모든 오리들이 quack() 메소드 수행하여 꽥소리를 낸다.

이제 오리의 세계에서는 어댑터를 이용해 거위를 오리처럼 다루는게 가능해졌고, 데코레이터를 이용해 오리가 꽥꽥 거리는 회수를 세는게 가능해졌고, 팩토리를 이용해 데코레이터의 기능을 확실하게 사용하기 위해 오리를 포장하는게 가능해졌고, 이터레이터와 컴포지트를 이용해 오리떼를 관리하는게 가능해졌다. 마지막으로 꽥학자들이 오리들이 quack() 메소드를 수행하여 꽥소리를 냈을 때, 어떤 오리가 꽥소리를 냈는지 연락을 받고싶다고 한다.

옵저버 패턴을 적용한 개별 오리 관리

어느 꽥학자가 오리들을 개별 단위로 관리하고 싶어한다. 오리들이 꽥소리를 냈을 때, 어떤 오리가 낸 소리인지 연락을 받고 싶다고 한다. 꽥꽥거리는 오리를 실시간으로 추적하기 위해 옵저버 패턴을 적용해보자.

  • 연락을 전달하는 주제를 의미하는 Observable 인터페이스
public interface QuackObservable {
    public void registerObserver(Observer observer); // 옵저버 등록
    public void notifyObservers(); // 연락 송신
}
  • 꽥소리를 낼 수 있는 Quakable이 주제가 된다.
public interface Quackable extends QuackObservable {
	public void quack();
}
  • 이번엔 옵저버 패턴의 정석과는 다르게, 일일이 등록 및 연락용 메소드를 구현하지 않고 Observable 보조 클래스에 캡슐화한다.
public class Observable implements QuackObservable {
    ArrayList<Observer> observers = new Observer();
    QuackObservable duck;

    public Observable(QuackObservable duck) { // 주제 설정
        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);
        }
    }
}
  • Observable 보조 클래스를 오리와 결합
public class MallardDuck implements Quackable{
   Observable observable;

   public MallardDuck(){
      observable = new Observable(this);
   }

   public void quack(){
      System.out.println("Quack");
      notifyObservers();
   }

   public void registerObserver(Observer observer){
      observable.registerObserver(observer)
   }

   public void notifyObservers(){
      observable.notifyObservers();
   }
}

위 코드는 옵저버의 정석적인 디자인과는 약간 다르게 구성되었다. 주제가 될 모든 오리에서 일일히 옵저버를 등록하고 연락을 전달하는 메소드를 구현하는건 비효율적이다. 효율성을 높이기 위해 Observable이라는 보조 클래스에 해당 작업들을 캡슐화한다. 이렇게 하면 실제 코드는 한 군데에만 작성해 놓고, 필요한 작업을 해당 클래스에 위임할 수 있다.

  • 연락을 전달받을 옵저버
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.");
    }
}

꽥학자는 이제 옵저버로서 시스템에 참여한다.

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

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

어댑터 패턴을 이용해 거위를 Quackable에 맞게 구현했다. 거위를 어댑터로 감싸면서 quack() 메소드를 호출하면 자동으로 honk() 메소드가 호출된다.

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

데코레이터 패턴을 이용해서 QuackCounter 데코레이터를 추가했다. quack() 메소드 호출 자체는 그 데코레이터로 싸여져 있는 Quackable에 의해 처리한 뒤, 카운터 데코레이터가 호출된 회수를 셀 수 있게 되었다.

꽥학자들이 QuackCounter로 장식되지 않은 Quackble 객체가 있을 수도 있음을 걱정

추상 팩토리 패턴을 적용해서 객체를 만들도록 구성했다. 오리 객체를 만들 때는 팩토리를 이용해야한다.

모든 Quackable 객체들을 관리하는 게 힘들어지기 시작

컴포지트 패턴을 적용해서 오리들을 모아서 오리떼 단위로 관리하도록 구성했다. 수많은 오리들을 부분적으로 관리할 수도 있으며 이터레이터를 이용해 반복작업까지 처리할 수 있게 되었다.

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

옵저버 패턴을 적용해서 Quackologist를 Quackable의 옵저버로 등록하여 연락을 받을 수 있도록 구성했다. Quackable에서 꽥소리를 낼 때마다 연락을 받을 수 있게 되었으며 주제에서 연락을 돌릴 때 이터레이터 패턴을 이용했다.

의문점

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

오리 프로그램은 그냥 여러가지 패턴을 섞어서 사용했을 뿐, 컴파운드 패턴이라고 할 순 없다. 컴파운드 패턴이라고 할 수 있으려면 몇 개의 패턴을 복합적으로 사용해서 일반적인 문제를 해결할 수 있는 것이어야 한다.

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

앞에서 여러 가지 패턴을 사용한 이유는 그냥 여러 패턴을 함께 사용할 수 있다는 것을 보여준 예시일 뿐이다. 실전에서는 이런 식으로 디자인을 하는 일은 없을 것이다. 상황에 따른 올바른 객체지향 디자인 원칙을 적용하는 것만으로도 문제가 해결되기도 한다. 중요한 것은 패턴은 반드시 상황에 맞게 써야 한다는 것이다.