[디자인패턴] 12. Compound Patterns

StandingAsh·2024년 12월 5일
3

참고: Head First Design Patterns

개요


Compound [동사]
섞다, 혼합하다

Compound Pattern 이라는 별도의 디자인 패턴 이름이 존재하는 것은 아니다. 말 그대로 여러 디자인 패턴들을 섞었다는 의미이다. 디자인 패턴은 하나만 사용할 때 보다 여러 패턴을 함께 사용했을 때 더 의미가 있는 경우가 많다. 여러 패턴들이 어떻게 함께 사용될 수 있는지 살펴보자

패턴들의 혼합 사용


오리 시뮬레이터

Strategy 패턴에서 살펴보았던 오리 예제를 다시 이용해보자.

public interface Quackable {
	public void quack();
}

우선 Quackable 인터페이스이다. 이전과 달리 이번엔 오리들이 이 인터페이스를 구현하도록 만들어보자.

public class MallardDuck implements Quackable {
	public void quack() {
		System.out.println(Quack);
	}
}

public class RedheadDuck implements Quackable {
	public void quack() {
		System.out.println(Quack);
	}
}

이제 오리 구현체들을 몇가지 더 추가해보자. 가령, 사냥꾼들이 쓰는 오리 피리러버덕 이라던가.

public class DuckCall implements Quackable {
    public void quack() {
        System.out.println(Kwak);
    }
}

public class RubberDuck implements Quackable {
    public void quack() {
        System.out.println(Squeak);
    }
}

이번엔 거위를 한번 구현해보자.

public class Goose {
	public void honk() {
		System.out.println(Honk);
	}
}

오리의 quack와 달리 거위의 울음은 honk라고 묘사한다. 따라서, 메소드가 호환이 되지 않으므로 거위를 오리처럼 다루기 위해서는 어댑터(Adapter) 클래스가 필요하다.

public class GooseAdapter implements Quackable {
    Goose goose;
 
    public GooseAdapter(Goose goose) {
        this.goose = goose;
    }
 
    public void quack() { goose.honk(); }
}

Decorator 패턴

어떤 이유에서인지 프로그램의 오리들의 quack() 호출의 총 횟수를 세고 싶어 졌다. 우리의 구현체들을 수정하지 않고 어떻게 이 기능을 구현할 수 있을까?

  • Decorator 클래스를 이용해, quack()의 횟수를 세는 로직을 추가하자!
public class QuackCounter implements 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; }
}

전역 변수 numberOfQuacks를 이용해 quack() 호출 시 마다 1씩 증가하도록 만들고, getter 메소드를 추가하였다. 이 데코레이터 클래스는 Quackable 타입을 장식하여 우리가 원하는 횟수 세기 기능을 추가해 줄 것이다.

따라서, 클라이언트는 오리를 아래와 같이 선언하게 된다.

Quackable mallardDuck = new QuackCounter(new MallardDuck());
Quackable redheadDuck = new QuackCounter(new RedheadDuck());
...

아, 물론 거위는 quack() 하지 않으므로 포함하지 않는다. 그래도 어댑터를 이용해 Quackable 타입으로 선언해주자.

Quackable gooseDuck = new GooseAdapter(new Goose());

quack() 횟수는 아래와 같이 호출할 수 있다.

System.out.println("총 횟수:+ QuackCounter.getQuacks() + “ 번”);

Factory 패턴

위 예제에는 한 가지 맹점이 있다. quack()을 카운팅하기 위해서는 Duck 구현체들이 반드시 QuackCounter 데코레이터 클래스로 감싸져있어야만 한다. 만일 그렇지 못한 구현체가 있다면 카운팅을 할 수 없다.

  • 그렇다면, Duck의 생성 로직을 한 객체에 몰아넣자!

마침 우리가 배운 Factory 패턴이 적당하다. 추상 팩토리 패턴을 이용해보자.

public abstract class AbstractDuckFactory {
	public abstract Quackable createMallardDuck();
	public abstract Quackable createRedheadDuck();
	public abstract Quackable createDuckCall();
	public abstract Quackable createRubberDuck();
}

이제 구현체를 만들어보자.

public class DuckFactory extends AbstractDuckFactory {
    public Quackable createMallardDuck() { return new MallardDuck(); }
    public Quackable createRedheadDuck() { return new RedheadDuck(); }
    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 createRedheadDuck() { return new QuackCounter(new RedheadDuck()); }
    public Quackable createDuckCall() { return new QuackCounter(new DuckCall()); }
    public Quackable createRubberDuck() { return new QuackCounter(new RubberDuck()); }
}

Duck 구현체와 Counter 구현체의 팩토리 클래스를 각각 만들었다. 이제 아래와 같이 클라이언트 코드를 수정할 수 있겠다.

AbstractDuckFactory duckFactory = new CountingDuckFactory();
Quackable mallardDuck = duckFactory.createMallardDuck();
Quackable redheadDuck = duckFactory.createRedheadDuck();
...

Iterator 패턴 & Composite 패턴

이제 생성 로직까지 잘 정리하였다. 그런데, 이 수많은 오리들을 어떻게 효율적으로 관리할 수 있을까? 역시 Iterator 만한 게 없다. 여기에 함께 배웠던 Composite 패턴도 적용하여 오리들의 quack()을 일괄 호출할 수 있는 오리 군집을 구현해보자.

public class Flock implements Quackable {
    ArrayList quackers = new ArrayList();
 
    public void add(Quackable quacker) { quackers.add(quacker); }
 
    public void quack() {
        Iterator iterator = quackers.iterator();
        while (iterator.hasNext()) {
            Quackable quacker = (Quackable)iterator.next();
            quacker.quack();
        }
    }
}

이제 Flockquack() 한 번으로 군집이 담고있는 모든 오리들의 quack()을 일괄적으로 호출할 수 있게 되었다. 물론, Flock 역시 Quackable의 구현체이므로, Flock은 또 다른 Flock 인스턴스들을 자식으로 가질 수 있다. Composite 패턴의 장점을 잘 담고 있다.

Observer 패턴

오리들을 한 번에 제어하는 것도 물론 중요하지만, 각각의 오리들을 관찰할 필요도 있다. 당연히, Observer 패턴이 등장 할 차례이다.

public interface QuackObservable {
	public void registerObserver(Observer observer);
	public void notifyObservers();
}

우선 Observable 클래스, 즉 Subject에 해당하는 인터페이스를 만들어보자.

public interface Quackable extends QuackObservable {
	public void quack();
}

그리고, 구현해두었던 Quackable 인터페이스가 이를 상속하도록 extends 코드를 추가하자. 이렇게 함으로써 구현체들의 코드에는 손 대지 않아도 된다.

이전 Observer 패턴을 다루면서 구현하였던 내용과는 조금 다르게 구현해보자. Observable 구현체를 만들어보겠다.

public class Observable implements QuackObservable {
    ArrayList observers = new ArrayList();
    QuackObservable duck;
 
    public Observable(QuackObservable duck) {
        this.duck = duck;
    }
  
    public void registerObserver(Observer observer) { observers.add(observer); }
  
    public void notifyObservers() {
        Iterator iterator = observers.iterator();
        while (iterator.hasNext()) {
            Observer observer = (Observer)iterator.next();
            observer.update(duck);
        }
    }
}

이제, 구성(Composition)을 이용하여 Quackable 구현체들이 이 객체를 갖도록 만들것이다.

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은 생성자를 통해 주어지고, Quackable 구현체들은 이 주어진 Observable을 통해 관찰자들을 notify() 해준다.
추가로, QuackCounter 클래스도 마찬가지로 register, notify 메소드를 구현해주어야 한다. Quackable 인터페이스를 구현하기 때문이다.

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.);
	}
}

마지막으로 옵저버 인터페이스와 구현체이다. update() 메소드를 통해 quack()을 한 객체, 즉 울음소리를 낸 오리를 전달 받는다.

정리


위 예제를 통해 하나의 프로그램에 어떻게 서로 다른 여러 디자인 패턴들이 적용될 수 있는지 알아보았다. 이런 디자인 패턴을 적절하게 적용할 줄 안다면 보다 명료하고 효율적인 프로그램을 만들 수 있을 것이다.

참고: MVC 패턴

스프링 부트 개발 경험이 있다면 MVC 패턴이라는 표현이 아주 친숙할 것이다. 그렇지 않은 사람들에게는 다소 생소할 수 있다. 물론 스프링 웹 프로젝트 경험이 있어도 생소할 수 있지만...

MVC패턴은 Model - View - Controller 3단계로 UI(유저 인터페이스)를 나누어 구현하는 구조이다. 그러나, 너무 어렵게 생각할 필요 없다. 결국 MVC 패턴도 디자인 패턴들의 모둠이니까!

  • 뷰(View)는 실제 사용자에게 보여질 UI이다.

  • 모델(Model)은 데이터와 서비스 로직을 담고 있다.

  • 컨트롤러(Controller)는 유저에게서 입력 값을 받아 모델에게 전달해준다.

이들의 관계를 지금까지 배운 디자인 패턴으로 나타내자면, 아래와 같다.

  • 컨트롤러는 전형적인 Strategy 패턴이다. 뷰는 전적으로 컨트롤러가 부여하는 Strategy에 의존한다.

  • Composite 패턴으로 구현되어있다. UI의 창, 패널 등의 요소는 Composite이며, 버튼 등은 leaf라고 할 수 있다.

  • 모델Observer 패턴에 해당한다. 상태 변화가 생기면 객체 정보를 update 받을 수 있도록 되어있다.

profile
우당탕탕 백엔드 생존기

0개의 댓글