읽은 책 정리/Head Firstr Design Pattern

CHAPTER 12.복합 패턴

복합 패턴이란?

  • 문제를 해결하기 위해 개발할 때는 효율적인 코드를 작성하기 위해 디자인 패턴을 고민하고 적용합니다.
  • 이때 하나의 디자인 패턴만을 사용하지 않고 여러 패턴을 복합적으로 사용하여 문제에 대한 해결을 시도 하는 것을 복합 패턴이라고 합니다.

복합 패턴 적용하기

  • 전략 패턴에서 사용했던 오리 예제를 다시 한 번 사용하여 예제를 진행하겠습니다.
  • 이전에는 오리 문제를 전략패턴만 사용해서 적용했지만 이번에는 새로운 문제를 예제 중간에 지속적으로 제안하고 문제 해결을 위한 디자인 패턴을 적용하도록 해보겠습니다.
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 RubberDuck implements Quackable {
	public void quack() {
		System.out.println("Squeak");
	}
}

public class DuckCall implements Quackable {
	public void quack() {
		System.out.println("Kwak");
	}
}
  • 오리 소리를 내는 행동을 정의하여 구현한 오리들의 구현체입니다.

테스트

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		simulator.simulate();
	}
  
	void simulate() {
		Quackable mallardDuck = new MallardDuck();
		Quackable redheadDuck = new RedheadDuck();
		Quackable duckCall = new DuckCall();
		Quackable rubberDuck = new RubberDuck();
 
		System.out.println("\nDuck Simulator");
 
		simulate(mallardDuck);
		simulate(redheadDuck);
		simulate(duckCall);
		simulate(rubberDuck);
	}
   
	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

public class Goose {
	public void honk() {
		System.out.println("Honk");
	}
}
  • 위 예제 상황에서 클라이언트가 위와 같은 Goose 객체를 추가하고 싶다고 추가 요구 사항을 제안 합니다.
  • 그러나 Goose 객체는 honk() 메소드를 통해 소리를 내는 메소드를 수행하기 때문에 인터페이스가 다르는 문제점이 존재합니다.
  • 이러한 문제를 해결하기 위해 어댑터 패턴을 적용하도록 하겠습니다.
public class GooseAdapter implements Quackable {
	Goose goose;
 
	public GooseAdapter(Goose goose) {
		this.goose = goose;
	}
 
	public void quack() {
		goose.honk();
	}

	public String toString() {
		return "Goose pretending to be a Duck";
	}
}

테스트

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		simulator.simulate();
	}

	void simulate() {
		Quackable mallardDuck = new MallardDuck();
		Quackable redheadDuck = new RedheadDuck();
		Quackable duckCall = new DuckCall();
		Quackable rubberDuck = new RubberDuck();
		//Goose를 GooseAdpate로 감싸서 다른 오리처럼 Quack메소드를 수행하도록 구현
		Quackable gooseDuck = new GooseAdapter(new Goose());
 
		System.out.println("\nDuck Simulator: With Goose Adapter");
 
		simulate(mallardDuck);
		simulate(redheadDuck);
		simulate(duckCall);
		simulate(rubberDuck);
		simulate(gooseDuck);
	}
 
	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

  • 위와 같은 상황에서 클라이언트는 꽥꽥 소리를 낸 횟수를 세주는 기능이 필요하다는 추가 사항을 요구합니다.
  • 새로운 행동을 추가 하기 위해서는 데코레이터 패턴을 적용하여 횟수를 측정하는 기능을 추가하면 기존의 Duck 코드는 변경하지 않아도 됩니다.
  • 그렇기 때문에 현재 예제에서 데코레이터 패턴을 추가하도록 하겠습니다.
public class QuackCounter implements Quackable {
	Quackable duck;
	static int numberOfQuacks;
  
	public QuackCounter (Quackable duck) {
		this.duck = duck;
	}
  
	public void quack() {
		duck.quack();
		numberOfQuacks++;
	}
 // Quack 소리가 몇 번 발생했는지를 반환한다.
	public static int getQuacks() {
		return numberOfQuacks;
	}
	public String toString() {
		return duck.toString();
	}
}
  • QuackCounter가 데코레이터 패턴을 의미하는 클래스입니다.
  • QuackCounter는 QuackAble 인터페이스를 정의하여 오리 객체를 생성할때 구현체를 생성자로 받아 메소드를 호출할때 quack() 메소드 수행과 회수를 측정하는 로직을 재정의합니다.

테스트

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		simulator.simulate();
	}

	void simulate() {
		Quackable mallardDuck = new QuackCounter(new MallardDuck());
		Quackable redheadDuck = new QuackCounter(new RedheadDuck());
		Quackable duckCall = new QuackCounter(new DuckCall());
		Quackable rubberDuck = new QuackCounter(new RubberDuck());
		Quackable gooseDuck = new GooseAdapter(new Goose());

		System.out.println("\nDuck Simulator: With Decorator");

		simulate(mallardDuck);
		simulate(redheadDuck);
		simulate(duckCall);
		simulate(rubberDuck);
		simulate(gooseDuck);

		System.out.println("The ducks quacked " + 
		                   QuackCounter.getQuacks() + " times");
	}

	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

  • 위와 같은 상황에서 클라이언트가 오리 구현체를 생성자로 넘겨 QuackCounter를 사용하는 방식에서 생성자를 전달하는 절차를 빼먹고 구현하여 휴먼에러가 발생할 수 있는점을 문제삼았습니다.
  • 오리 객체를 생성하는 작업을 한 곳에서 하면 이런 문제가 발생하지 않겠다고 판단하여 데코레이터 패턴을 적용하는 부분에 캡슐화하는 작업을 진행하기 위해 팩토리 패턴을 적용하기로 결정하였습니다.
public abstract class AbstractDuckFactory {
 
	public abstract Quackable createMallardDuck();
	public abstract Quackable createRedheadDuck();
	public abstract Quackable createDuckCall();
	public abstract Quackable createRubberDuck();
}
  • 여러 종류의 오리를 생성하므로 추상 팩토리 패턴을 적용하도록 해보겠습니다.
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());
	}
}
  • AbstractDuckFactory를 상속하여 정의한 CountingDuckFactory 클래스입니다.

테스트

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		AbstractDuckFactory duckFactory = new CountingDuckFactory();
 
		simulator.simulate(duckFactory);
	}
 
//객체의 인스턴스를 직접 생성하지하고 팩토리 메소드를 통해 생성합니다.
	void simulate(AbstractDuckFactory duckFactory) {
		Quackable mallardDuck = duckFactory.createMallardDuck();
		Quackable redheadDuck = duckFactory.createRedheadDuck();
		Quackable duckCall = duckFactory.createDuckCall();
		Quackable rubberDuck = duckFactory.createRubberDuck();
		Quackable gooseDuck = new GooseAdapter(new Goose());
 
		System.out.println("\nDuck Simulator: With Abstract Factory");
 
		simulate(mallardDuck);
		simulate(redheadDuck);
		simulate(duckCall);
		simulate(rubberDuck);
		simulate(gooseDuck);
 
		System.out.println("The ducks quacked " + 
		                   QuackCounter.getQuacks() +
		                   " times");
	}
 
	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

  • 위와 같은 상황에서 클라이언트가 simulate() 코드가 반복되는것에 최적화를 요구했습니다.
  • 또한, 오리를 일괄적으로 관리할 수 있는 방법이 필요하다는 개발 추가 사항이 접수되었습니다.
  • 이러한 문제를 해결하기 위해 컴포지트 패턴을 적용하도록 해보겠습니다.
public class Flock implements Quackable {
	ArrayList<Quackable> quackers = new ArrayList<Quackable>();
 
	public void add(Quackable quacker) {
		quackers.add(quacker);
	}
 
	public void quack() {
		Iterator<Quackable> iterator = quackers.iterator();
		while (iterator.hasNext()) {
			Quackable quacker = iterator.next();
			quacker.quack();
		}
	}
 
	public String toString() {
		return "Flock of Quackers";
	}
}
  • 컴포지트 패턴의 복합 객체와 잎 원소는 같은 인터페이스를 구현해야하기 때문에 Quackable 인터페이스를 구현하고 있습니다.
  • Flcok에 속하는 Quackable 객체들은 ArrayList에 저장하고 있습니다.
  • quack() 메소드를 살펴보면 객체의 Iteratort를 반환하여 재귀적으로 순환을 돌려서 호출하는것을 확인할 수 있습니다. 이러한 부분은 반복자 패턴을 적용하여 구현하였습니다.

테스트

public class DuckSimulator {

	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		AbstractDuckFactory duckFactory = new CountingDuckFactory();
 
		simulator.simulate(duckFactory);
	}
 
	void simulate(AbstractDuckFactory duckFactory) {
		Quackable redheadDuck = duckFactory.createRedheadDuck();
		Quackable duckCall = duckFactory.createDuckCall();
		Quackable rubberDuck = duckFactory.createRubberDuck();
		Quackable gooseDuck = new GooseAdapter(new Goose());

		System.out.println("\nDuck Simulator: With Composite - Flocks");
		//Flock을 생성한 다음 Quackable을 Flock의 리스트에 포함시킵니다.
		Flock flockOfDucks = new Flock();

		flockOfDucks.add(redheadDuck);
		flockOfDucks.add(duckCall);
		flockOfDucks.add(rubberDuck);
		flockOfDucks.add(gooseDuck);

		//물에서만 뜨는 오리를 담는 Flock을 별도로 생성합니다.
		Flock flockOfMallards = new Flock();

		Quackable mallardOne = duckFactory.createMallardDuck();
		Quackable mallardTwo = duckFactory.createMallardDuck();
		Quackable mallardThree = duckFactory.createMallardDuck();
		Quackable mallardFour = duckFactory.createMallardDuck();

		flockOfMallards.add(mallardOne);
		flockOfMallards.add(mallardTwo);
		flockOfMallards.add(mallardThree);
		flockOfMallards.add(mallardFour);

		//처음에 생성한 Flock에 물 오리 Flock을 함시킵니다.
		flockOfDucks.add(flockOfMallards);

		//Flock +  물 오리 Flock이 동시적으로 호출됩니다.
		System.out.println("\nDuck Simulator: Whole Flock Simulation");
		simulate(flockOfDucks);

		//물 오리 Flcok만 호출됩니다.
		System.out.println("\nDuck Simulator: Mallard Flock Simulation");
		simulate(flockOfMallards);

		System.out.println("\nThe ducks quacked " + 
		                   QuackCounter.getQuacks() +
		                   " times");
	}

	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

  • 이런 상황에서 오리들의 개별 행동들을 관찰하고 싶다는 클라이언트의 추가 개발 요구 사항이 접수되었습니다.
  • 이러한 요구사항을 충족시키기 위해 옵저버 패턴을 적용해보겠습니다.
public interface QuackObservable {
	public void registerObserver(Observer observer);
	public void notifyObservers();
}
  • 관찰 대상이 되는것이 Observable 인터페이스입니다. Observable에는 옵저버를 등록하는 메소드와 옵저버에게 연락을 돌리는 메소드가 존재합니다.
  • 옵저버를 제거하는 메소드는 별도로 필요시 구현하는 방향으로 예제를 진행하겠습니다.
public interface Quackable extends QuackObservable {
	public void quack();
}
  • 모든 Quackable은 관찰 대상이 되므로 Quackable에서 Observable 인터페이스를 상속받도록 구현하겠습니다.
public class Observable implements QuackObservable {
	List<Observer> observers = new ArrayList<Observer>();
	QuackObservable duck;
 
	public Observable(QuackObservable duck) {
		this.duck = duck;
	}
  
	public void registerObserver(Observer observer) {
		observers.add(observer);
	}
  
	public void notifyObservers() {
		Iterator<Observer> iterator = observers.iterator();
		while (iterator.hasNext()) {
			Observer observer = iterator.next();
			observer.update(duck);
		}
	}
 
	public Iterator<Observer> getObservers() {
		return observers.iterator();
	}
}
  • 관찰 대상이 되는 QuackObservalble에 필요한 모든 기능을 구현하는 Observable 객체입니다.
  • Observable은 각 오리 객체에 존재하면서 옵저버에게 필요한 행동들을 수행합니다.
  • Observable은 QuackObservable을 구현합니다. 나중에 QuackObservable 에서 정의한 메소드를 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();
	}
 
	public String toString() {
		return "Mallard Duck";
	}
}
  • 위와 같이 관찰 대상에 Observable 객체를 추가합니다.
  • MallardDuck 외에도 다른 오리 객체에도 Quackable을 정의를 구현하고 Observable 객체를 선언해야합니다.
public interface Observer {
	public void update(QuackObservable duck);
}
  • 관찰자인 Observer 인터페이스를 선언해줍니다.
  • 관찰자인 Observer 인터페이스는 관찰 대상인 Observable 객체가 수행될 때 Observer의 update 메소드가 수행됩니다.
public class Quackologist implements Observer {
 
	public void update(QuackObservable duck) {
		System.out.println("Quackologist: " + duck + " just quacked.");
	}
 
	public String toString() {
		return "Quackologist";
	}
}
  • Observer 인터페이스의 구현체인 Quacklogist 입니다.

테스트

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		AbstractDuckFactory duckFactory = new CountingDuckFactory();
 
		simulator.simulate(duckFactory);
	}
  
	void simulate(AbstractDuckFactory duckFactory) {
  
		Quackable redheadDuck = duckFactory.createRedheadDuck();
		Quackable duckCall = duckFactory.createDuckCall();
		Quackable rubberDuck = duckFactory.createRubberDuck();
		Quackable gooseDuck = new GooseAdapter(new Goose());
 
		Flock flockOfDucks = new Flock();
 
		flockOfDucks.add(redheadDuck);
		flockOfDucks.add(duckCall);
		flockOfDucks.add(rubberDuck);
		flockOfDucks.add(gooseDuck);
 
		Flock flockOfMallards = new Flock();
 
		Quackable mallardOne = duckFactory.createMallardDuck();
		Quackable mallardTwo = duckFactory.createMallardDuck();
		Quackable mallardThree = duckFactory.createMallardDuck();
		Quackable mallardFour = duckFactory.createMallardDuck();

		flockOfMallards.add(mallardOne);
		flockOfMallards.add(mallardTwo);
		flockOfMallards.add(mallardThree);
		flockOfMallards.add(mallardFour);

		flockOfDucks.add(flockOfMallards);

		System.out.println("\nDuck Simulator: With Observer");

		Quackologist quackologist = new Quackologist();
		flockOfDucks.registerObserver(quackologist);

		simulate(flockOfDucks);

		System.out.println("\nThe ducks quacked " + 
		                   QuackCounter.getQuacks() +
		                   " times");
	}
 
	void simulate(Quackable duck) {
		duck.quack();
	}
}

수행결과

예제 클래스 다이어그램

복합 패턴인 MVC 패턴 분석해보기

  • MVC 패턴은 Model-View-Controller라는 의미입니다.
  • 각 단계가 뷰 → 컨트롤러 → 모델 3단의 관계로 이루어져있습니다.
  • 뷰 영역은 사용자가 보는 인터페이스 화면을 의미하며 사용자는 뷰에만 접족할 수 있습니다. 사용자가 뷰 영역에서 행동을 취할 경우 컨틀롤러 영역으로 입력이 전달됩니다.
  • 컨트롤러에서는 입력 받은 내용에 대한 행동을 수행합니다. 컨트롤러에서 수행한 결과로 생긴 데이터와 상태 값들은 모델이라는 영역으로 데이터를 전달합니다.
  • 변경된 정보를 가지고 있는 모델은 다시 한 번 뷰에게 새로운 정보를 전달해줍니다.
  • 컨트롤러에서 뷰로 데이터를 변경해 달라고 요청하는 경우도 존재합니다. 예를 들어 컨트롤러를 통해 인터페이스의 버튼이나 메뉴를 비활성화할 수 있습니다.

  • 이러한 MVC 패턴은 크게 전략 패턴, 컴포지트 패턴, 옵저버 패턴 3가지로 결합된 패턴입니다.
  • 옵저버 패턴은 모델의 상태가 변할 때마다 뷰와, 1컨트롤러에게 연락하기 위해 사용됩니다.
  • 전략 패턴은 뷰와 컨트롤러의 관계에서 사용됩니다. 컨트롤러는 전략 패턴의 행동에 해당하며 사용자의 요청에 맞게 행동이 변환되는 전략패턴의 특성 처럼 사용자의 요청에 따라 다른 컨트롤러가 동작합니다.
  • 컴포지트 패턴은 뷰안에서 사용됩니다. 버튼, 윈도우 와 같은 다양한 요소를 컴포지트 패턴을 통해 관리하게 됩니다.

MVC 패턴 구현해보기

  • 오디오 증감 프로그램을 MVC 패턴을 활용해서 구현해보겠습니다.

요구사항

  • 위 그림처럼 >> 버튼을 클릭하면 1 BPM이 상승합니다.
  • 반대로 << 버튼을 클릭하면 1 BPM이 감소합니다.
  • Set 버튼을 누를 경우 설정한 수치만큼 BPM 값이 변경됩니다.
public interface BeatModelInterface {
	//컨트롤러가 모델에게 사용자 입력을 전달할 때 사용하는 메소드
	void initialize();
  //컨트롤러가 모델에게 사용자 입력을 전달할 때 사용하는 메소드
	void on();
  //컨트롤러가 모델에게 사용자 입력을 전달할 때 사용하는 메소드
	void off();
  //컨트롤러가 모델에게 사용자 입력을 전달할 때 사용하는 메소드
  void setBPM(int bpm); //BPM을 설정하는 메소드
  //밑에 메소드 모두 뷰와 컨트롤러가 상태를 알아내거나 옵저버로 등록할 때사용하는 메소드
	int getBPM(); //현재 BPM을 반환 비트 생성기가 꺼저 있을경우 0을 반환
  
	void registerObserver(BeatObserver o);
  
	void removeObserver(BeatObserver o);
  
	void registerObserver(BPMObserver o);
  
	void removeObserver(BPMObserver o);
}
  • BeatModelnterface는 컨트롤러에서 비트를 조절하거나 뷰와 컨트롤러에서 모델의 상태를 알아낼 때 사용할 수 있도록 외부에 공개된 인터페이스입니다.
public class BeatModel implements BeatModelInterface, Runnable {
	List<BeatObserver> beatObservers = new ArrayList<BeatObserver>();
	List<BPMObserver> bpmObservers = new ArrayList<BPMObserver>();
	int bpm = 90;
	Thread thread;
	boolean stop = false; //비트 재생 여부 관련 flag
	Clip clip; //비트용으로 재생하는 오디오 Clip

	public void initialize() {
		try {
			File resource = new File("clap.wav");
			clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));
			clip.open(AudioSystem.getAudioInputStream(resource));
		}
		catch(Exception ex) {
			System.out.println("Error: Can't load clip");
			System.out.println(ex);
		}
	}

	public void on() {
		bpm = 90;
		//notifyBPMObservers();
		thread = new Thread(this);
		stop = false;
		thread.start();
	}

	public void off() {
		stopBeat();
		stop = true;
	}

	public void run() {
		while (!stop) {
			playBeat();
			notifyBeatObservers();
			try {
				Thread.sleep(60000/getBPM());
			} catch (Exception e) {}
		}
	}

	public void setBPM(int bpm) {
		this.bpm = bpm;
		notifyBPMObservers();
	}

	public int getBPM() {
		return bpm;
	}

	public void registerObserver(BeatObserver o) {
		beatObservers.add(o);
	}

	public void notifyBeatObservers() {
		for(int i = 0; i < beatObservers.size(); i++) {
			BeatObserver observer = (BeatObserver)beatObservers.get(i);
			observer.updateBeat();
		}
	}

	public void registerObserver(BPMObserver o) {
		bpmObservers.add(o);
	}

	public void notifyBPMObservers() {
		for(int i = 0; i < bpmObservers.size(); i++) {
			BPMObserver observer = (BPMObserver)bpmObservers.get(i);
			observer.updateBPM();
		}
	}

	public void removeObserver(BeatObserver o) {
		int i = beatObservers.indexOf(o);
		if (i >= 0) {
			beatObservers.remove(i);
		}
	}

	public void removeObserver(BPMObserver o) {
		int i = bpmObservers.indexOf(o);
		if (i >= 0) {
			bpmObservers.remove(i);
		}
	}

	public void playBeat() {
		clip.setFramePosition(0);
		clip.start();
	}
	public void stopBeat() {
		clip.setFramePosition(0);
		clip.stop();
	}

}
  • BeatModelInterface의 구현체인 BeatModel입니다.
public class DJView implements ActionListener, BeatObserver, BPMObserver {
		BeatModelInterface model;
		ControllerInterface controller;
    JFrame viewFrame;
    JPanel viewPanel;
		BeatBar beatBar;
		JLabel bpmOutputLabel;
    JFrame controlFrame;
    JPanel controlPanel;
    JLabel bpmLabel;
    JTextField bpmTextField;
    JButton setBPMButton;
    JButton increaseBPMButton;
    JButton decreaseBPMButton;
    JMenuBar menuBar;
    JMenu menu;
    JMenuItem startMenuItem;
    JMenuItem stopMenuItem;

    public DJView(ControllerInterface controller, BeatModelInterface model) {	
		this.controller = controller;
		this.model = model;
		model.registerObserver((BeatObserver)this);
		model.registerObserver((BPMObserver)this);
    }
    
    public void createView() {
		// Create all Swing components here
        viewPanel = new JPanel(new GridLayout(1, 2));
        viewFrame = new JFrame("View");
        viewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        viewFrame.setSize(new Dimension(100, 80));
        bpmOutputLabel = new JLabel("offline", SwingConstants.CENTER);
				beatBar = new BeatBar();
				beatBar.setValue(0);
        JPanel bpmPanel = new JPanel(new GridLayout(2, 1));
				bpmPanel.add(beatBar);
        bpmPanel.add(bpmOutputLabel);
        viewPanel.add(bpmPanel);
        viewFrame.getContentPane().add(viewPanel, BorderLayout.CENTER);
        viewFrame.pack();
        viewFrame.setVisible(true);
	}
  

		//스윙 구성 요소  
    public void createControls() {
		// Create all Swing components here
        JFrame.setDefaultLookAndFeelDecorated(true);
        controlFrame = new JFrame("Control");
        controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        controlFrame.setSize(new Dimension(100, 80));

        controlPanel = new JPanel(new GridLayout(1, 2));

        menuBar = new JMenuBar();
        menu = new JMenu("DJ Control");
        startMenuItem = new JMenuItem("Start");
        menu.add(startMenuItem);
        startMenuItem.addActionListener((event) -> controller.start());
        // was....
        /*
        startMenuItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                controller.start();
            }
        });
        */
        stopMenuItem = new JMenuItem("Stop");
        menu.add(stopMenuItem); 
        stopMenuItem.addActionListener((event) -> controller.stop());
        // was...
        /*
        stopMenuItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                controller.stop();
            }
        });
        */
        JMenuItem exit = new JMenuItem("Quit");
        exit.addActionListener((event) -> System.exit(0));
        // was...
        /*
        exit.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                System.exit(0);
            }
        });
        */

        menu.add(exit);
        menuBar.add(menu);
        controlFrame.setJMenuBar(menuBar);

        bpmTextField = new JTextField(2);
        bpmLabel = new JLabel("Enter BPM:", SwingConstants.RIGHT);
        setBPMButton = new JButton("Set");
        setBPMButton.setSize(new Dimension(10,40));
        increaseBPMButton = new JButton(">>");
        decreaseBPMButton = new JButton("<<");
        setBPMButton.addActionListener(this);
        increaseBPMButton.addActionListener(this);
        decreaseBPMButton.addActionListener(this);

        JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
	
				buttonPanel.add(decreaseBPMButton);
				buttonPanel.add(increaseBPMButton);

        JPanel enterPanel = new JPanel(new GridLayout(1, 2));
        enterPanel.add(bpmLabel);
        enterPanel.add(bpmTextField);
        JPanel insideControlPanel = new JPanel(new GridLayout(3, 1));
        insideControlPanel.add(enterPanel);
        insideControlPanel.add(setBPMButton);
        insideControlPanel.add(buttonPanel);
        controlPanel.add(insideControlPanel);
        
        bpmLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
        bpmOutputLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));

        controlFrame.getRootPane().setDefaultButton(setBPMButton);
        controlFrame.getContentPane().add(controlPanel, BorderLayout.CENTER);

        controlFrame.pack();
        controlFrame.setVisible(true);
    }

	public void enableStopMenuItem() {
    	stopMenuItem.setEnabled(true);
	}

	public void disableStopMenuItem() {
    	stopMenuItem.setEnabled(false);
	}

	public void enableStartMenuItem() {
    	startMenuItem.setEnabled(true);
	}

	public void disableStartMenuItem() {
    	startMenuItem.setEnabled(false);
	}

    public void actionPerformed(ActionEvent event) {
		if (event.getSource() == setBPMButton) {
			int bpm = 90;
			String bpmText = bpmTextField.getText();
			if (bpmText == null || bpmText.contentEquals("")) {
				bpm = 90;
			} else {
				bpm = Integer.parseInt(bpmTextField.getText());
			}
        	controller.setBPM(bpm);
		} else if (event.getSource() == increaseBPMButton) {
			controller.increaseBPM();
		} else if (event.getSource() == decreaseBPMButton) {
			controller.decreaseBPM();
		}
    }

	public void updateBPM() {
		if (model != null) {
			int bpm = model.getBPM();
			if (bpm == 0) {
				if (bpmOutputLabel != null) {
        			bpmOutputLabel.setText("offline");
				}
			} else {
				if (bpmOutputLabel != null) {
        			bpmOutputLabel.setText("Current BPM: " + model.getBPM());
				}
			}
		}
	}
  
	public void updateBeat() {
		if (beatBar != null) {
			 beatBar.setValue(100);
		}
	}
}
  • 요구사항의 뷰를 담당하는 구현입니다.
public interface ControllerInterface {
	void start();
	void stop();
	void increaseBPM();
	void decreaseBPM();
 	void setBPM(int bpm);
}
  • 컨트롤러 인터페이스입니다.
public class BeatController implements ControllerInterface {
	BeatModelInterface model;
	DJView view;
   
	//컨트롤러 생성시 모델 인자가 전달되며 생성자에서 뷰를 생성한다.
	public BeatController(BeatModelInterface model) {
		this.model = model;
		view = new DJView(this, model);
    view.createView();
    view.createControls();
		view.disableStopMenuItem();
		view.enableStartMenuItem();
		model.initialize();
	}
  
	public void start() {
		model.on();
		view.disableStartMenuItem();
		view.enableStopMenuItem();
	}
  
	public void stop() {
		model.off();
		view.disableStopMenuItem();
		view.enableStartMenuItem();
	}
    
	public void increaseBPM() {
        int bpm = model.getBPM();
        model.setBPM(bpm + 1);
	}
    
	public void decreaseBPM() {
        int bpm = model.getBPM();
        model.setBPM(bpm - 1);
  	}
  
 	public void setBPM(int bpm) {
		model.setBPM(bpm);
	}
}
  • BeatController는 컨트롤러 인터페이스를 정의한 구현체입니다

테스트

public class DJTestDrive {

    public static void main (String[] args) {
        BeatModelInterface model = new BeatModel();
		ControllerInterface controller = new BeatController(model);
    }
}

수행결과

  • BPM set과 BPM 증감이 이뤄지는것이 정상적으로 작동한다.

정리

  • 복합 패턴은 2개 이상의 패턴을 결합한 패턴을 의미합니다.
  • MVC 패턴은 옵저버, 전략, 컴포지트 패턴으로 이루어진 복합 패턴입니다.
  • 모델은 옵저버 패턴을 사용해서 의존성을 없애면서 옵저버들에게 자신의 상태가 변경되었음을 알리수 있습니다.
  • 컨트롤러는 뷰의 전략 객체입니다. 뷰는 사용자의 요청에 맞게 적합한 컨트롤러를 수행합니다.
  • 뷰는 컴포지트 패턴을 사용해 사용자 인터페이스를 구현합니다. 패널, 프레임, 버튼과 같은 중첩 요소들이 구성 요소로 이루어집니다.
  • 새로운 모델을 기존 뷰와 컨트롤러에 연결할 경우에는 어댑터 패턴을 활용하면 됩니다.