-
디자인 패턴 스터디 기록 (7) - 복합 패턴과 MVC✏️ 스터디 모음집/디자인 패턴 스터디 2022. 12. 12.
복합 패턴의 필요성(feat. 오리 시뮬레이션 게임 만들기)
🐥 작고 귀여운 오리 시뮬레이터
public interface Quackable{ public void quack(); } void simulate(){ Quackable mallardDuck = new MallardDuck(); Quackable duckCall = new DuckCall(); Quackable rubberDuck = new rubberDuck(); simulate(mallardDuck); simulate(duckCall); simulate(rubberDuck); } void simulate(Quackable duck){ duck.quack(); //어떤 오리든 quack()을 호출해서 테스트 가능(다형성) }
📢 거위용 시뮬레이터도 만들어주세요 거위도 오리랑 비슷하잖아요!
거위 클래스를 호환 하기 위한 어댑터 추가 ← 어댑터 패턴
public class Goose { public void honk() { system.out.printIn("끽끽"); // 거위는 quack 대신 honk를 합니다. } } public class GooseAdapter implements Quackable { Goose goose; public GooseAdapter(Goose goose) { this.goose = goose; } //quack()메소드가 호출되면 goose의 honk() 메소드를 호출한다. public void quack() { goose.honk(); } } void simulate(){ . . Quackable gooseDuck = new GooseAdapter(new Goose()); //Goose를 어댑터로 감싸 Duck인척 한다. simulate(gooseDuck); }
📢 오오 그러면 꽥꽥 소리를 낸 횟수를 세는 기능도 추가해주세요! ← 데코레이터 패턴 추가
public class QuackCounter implements Quackable { Quackable duck; static int numberOfQuacks; //모든 객체의 회수를 세야하기 때문에 정적 변수 사용 public QuackCounter (Quackable duck) { this.duck = duck; } public void quack() { //quack 메소드를 호출하면 데코레이터 안에 있는 duck객체에 위임하고 //numberOfQuacks 를 증가한다. duck.quack(); numberOfQuacks++; } public static int getQuacks() { return numberOfQuacks; } } /*-----------------------------사용 예시---------------------------*/ void simulate(){ . . //Quackable 객체를 데코레이터로 감싸준다 Quackable mallardDuck = new QuackCounter(new MallardDuck()); Quackable duckCall = new QuackCounter(new DuckCall()); Quackable rubberDuck = new QuackCounter(new rubberDuck()); simulate(mallardDuck); simulate(duckCall); simulate(rubberDuck); System.out.println("The ducks quacked " + QuackCounter.getQuacks() + " times"); }
📢 추가로 오리를 생성하는 부분을 한곳에서 하면 어떨까요? ← 추상 팩토리 패턴 추가
//추상 팩토리 패턴으로 여러가지 오리들을 생산하는 팩토리 클래스 정의 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()); } } /*-----------------------------사용 예시---------------------------*/ //오리 생성 팩토리를 인자로 받음 void simulate(AbstractDuckFactory duckFactory){ . . //직접 객체를 생성하지 않고 팩토리의 메소드를 통해 생성 Quackable mallardDuck = duckFactory.createMallardDuck(); Quackable duckCall = duckFactory.createDuckCall(); Quackable rubberDuck = duckFactory.createRubberDuck(); simulate(mallardDuck); simulate(duckCall); simulate(rubberDuck); System.out.println("The ducks quacked " + QuackCounter.getQuacks() + " times"); }
📢 생성된 오리들을 한곳에서 종류별로 관리하고 싶어요! ← 컴포지트 패턴, 반복자 패턴
//복합객체는 잎원소와 똑같은 인터페이스를 구현함 = Quackable public class Flock implements Quackable { //ArrayList로 오리떼에 속하는 오리들을 보관함 ArrayList quackers = new ArrayList(); public void add(Quackable quacker) { quackers.add(quacker); } //Flock에 들어있는 모든 오리들의 quack()을 호출해주어야함 public void quack() { Iterator iterator = quackers.iterator(); while (iterator.hasNext()) { Quackable quacker = (Quackable)iterator.next(); quacker.quack(); } } public String toString() { return "Flock of Quackers"; } } /*-----------------------------사용 예시---------------------------*/ void simulate(AbstractDuckFactory duckFactory){ Quackable mallardDuck = duckFactory.createMallardDuck(); Quackable duckCall = duckFactory.createDuckCall(); Quackable rubberDuck = duckFactory.createRubberDuck(); Flock flockOfDucks = new Flock(); flockOfDucks.add(mallardDuck); flockOfDucks.add(duckCall); flockOfDucks.add(rubberDuck); //위에서 만든 오리떼 하나만 넣어주면 모든 오리에 대한 테스트가 가능해짐 simulate(flockOfDucks); System.out.println("The ducks quacked " + QuackCounter.getQuacks() + " times"); }
📢 실시간으로 오리를 각각 1마리씩 추적하고 싶으면요? ← 옵저버 패턴
public interface QuackObservable { //Observer 인터페이스를 구현하는 객체라면 어떤 객체는 꽥꽥거리는걸 감시할 수 있음 public void registerObserver(Observer observer); public void notifyObservers(); } //QuackObservable의 일을 위임해야하니까 구현하기 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); } } public Iterator getObservers() { return observers.iterator(); } } public class MallardDuck implements Quackable { Observable observable; public MallardDuck() { observable = new Observable(this); } public void quack() { System.out.println("Quack"); notifyObservers(); } //QuackObservable에서 정의한 메소드들 //모두 observable 보조 객체에 작업을 위임한다. public void registerObserver(Observer observer) { observable.registerObserver(observer); } public void notifyObservers() { observable.notifyObservers(); } public String toString() { return "Mallard Duck"; } } 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."); } } /*-----------------------------사용 예시---------------------------*/ void simulate(AbstractDuckFactory duckFactory){ //오리 객체 생성 //오리떼 객체 생성 Quackologist quackologist = new Quackologist(); floackOfDucks.registerObserver(quackologist); simulate(flockOfDucks); System.out.println("The ducks quacked " + QuackCounter.getQuacks() + " times"); }
“복합 패턴의 왕” MVC 패턴
M : Model
- 데이터와 상태를 가지고있고 그 데이터를 조작할 로직 또한 여기에 들어있다.
V : Veiw
- 모델을 표현하는 방법을 제공, 모델에서 데이터를 가져와 화면에 그리는 로직 담당
C: Controller
- 사용자로부터 입력을 받아 그 입력을 토대로 모델과 뷰 간의 상호작용을 처리
M,V,C 각 요소에 패턴 녹여내기
Model : 옵저버 패턴
- 상태값이 변경되었을때 그 모델과 연관된 뷰 또는 컨트롤러에게 연락 돌림 (옵저버 패턴)
- Model이 옵저버 패턴의 Subject, Model의 상태값의 변화를 알림받을 녀셕들인 Observer가 뷰, 컨트롤러들 이라고 할 수 있다.
Veiw : 컴포지트 패턴
- 디스플레이는 윈도우 → 패널 → 버튼 → 텍스트 레이블 등 여러개의 컴포넌트로 구성됩니다. 컨트롤러가 뷰에게 화면 갱신을 요청하면 최상위 뷰 구성요소에게만 화면을 갱신하라고 이야기 하면 됩니다.(컴포지트 패턴)
- Veiw는 옵저버 패턴에서 Observer! Subject인 모델이 변경될때 UI컴포넌트 들도 변경 시켜야 하므로 그때 모델이 호출할 함수를 제공해야 함.
Controller - Veiw : 전략패턴
- 뷰와 컨트롤러는 전략패턴으로 구성되어 모델로부터 뷰를 분리해 낼 수 있습니다.
- 뷰는 애플리케이션의 겉모습에만 신경을 쓰고, 모델을 처리하는 부분은 전적으로 모두 컨트롤러에게 맡깁니다. 뷰가 받은 사용자 입력으로 모델을 처리하는 부분은 캡슐화 되어 컨트롤러 안에서 처리되므로 뷰는 알지 못하고 알 필요도 없습니다.
MVC 패턴을 이용해서 BPM 제어 도구 만들기
Model 인터페이스 설계
아래 조건들을 만족하는지 봐볼까요?
⇒ (1) 옵저버(컨트롤러, 뷰)들을 저장할 컨테이너, 컨테이너에 옵저버들을 등록, 해제 하는 로직을 가지고 있을것
⇒ (2) 상태값을 가지고 있고, 상태값을 조작하는 함수들을 제공할것
⇒ (3) 상태값이 변경되었을때 옵저버들을 순회하며 호출 할것
public interface BeatModelInterface { /** * 컨트롤러에서 모델한테 사용자 입력을 전달할 때 사용하는 메서드들 */ // BeatModel의 인스턴스가 만들어질 때 호출되는 메서드 void initialize(); // 비트 생성기 on/off를 위한 메서드 void on(); void off(); // ✅ (2) 상태값을 가지고 있고, 상태값을 조작하는 함수들을 제공할것 // BPM을 설정하기 위한 메서드 // 이 메서드가 호출되면 비트수가 바로 바뀜 void setBPM(int bpm); /** * 뷰와 컨트롤러에서 상태를 알아내거나 옵저버로 등록할 때 사용할 메서드 */ // 현재 BPM을 리턴함. 비트 생성기가 꺼져있으면 0을 리턴 int getBPM(); // ✅ (1) 옵저버들을 저장할 컨테이너, 컨테이너에 옵저버들을 등록, 해제 하는 로직을 가지고 있을것 // 매 박자마다 연락받을 옵저버 등록과 해제 void registerObserver(BeatObserver o); void removeObserver(BeatObserver o); // BPM이 바뀔 때마다 연락 받을 옵저버 등록과 해제 void registerObserver(BPMObserver o); void removeObserver(BPMObserver o); }
. . . public class BeatModel implements BeatModelInterface, Runnable{ . . . List<BPMObserver> bpmObservers = new ArrayList<>(); int bpm = 90; // default bpm: 90 public void notifyBPMObservers(){ for(int i=0; i<bpmObservers.size(); i++){ BPMObserver observer = (BPMObserver)bpmObservers.get(i); observer.updateBPM(); } } @Override public void setBPM(int bpm) { this.bpm = bpm; // ✅ (3) 상태값이 변경되었을때 옵저버들을 순회하며 호출 할것 notifyBPMObservers(); } @Override public void registerObserver(BPMObserver o) { bpmObservers.add(o); } @Override public void removeObserver(BPMObserver o) { int i = bpmObservers.indexOf(o); if(i >= 0) bpmObservers.indexOf(o); } . . . }
View 인터페이스 설계
아래 조건들을 만족하는지 봐볼까요?
- 뷰는 윈도우 → 패널 → 버튼 → 텍스트 레이블 등 여러개의 UI 컴포넌트로 구성되고
- 각각의 컴포넌트를 직접 조작 합니다. (컴포지트 패턴)
- 뷰에서는 컴포넌트만 조작합니다 모델을 조작하는 처리는 뷰에서 안하고 컨트롤러에게 전달해 처리 합니다. (전략 패턴)
- Veiw는 옵저버 패턴에서 Observer! (옵저버 패턴)
- 옵저버 패턴에서 Subject 역할을 하는 Mode이 호출할 함수를 제공해야 함.
import java.awt.*; import java.awt.event.*; import javax.swing.*; // ✅ Veiw는 옵저버 패턴에서 Observer! public class DJView implements ActionListener, BeatObserver, BPMObserver { // 뷰에는 모델과 컨트롤러에 대한 참조가 모두 들어있다. // 컨트롤러에 대한 참조는 제어용 언테페이스 코드에서만 사용한다. BeatModelInterface model; ControllerInterface controller; // ✅ 뷰는 윈도우 → 패널 → 버튼 → 텍스트 레이블 등 여러개의 UI 컴포넌트로 구성되고 // 화면 표시용 구성요소 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.removeObserver((BPMObserver) this); } /** * 사용자가 버튼을 클릭했을 때 호출되는 메서드 */ public void actionPerformed(ActionEvent event) { // 사용자가 'Set' 버튼을 클릭하면 새로운 분당 비트수가 컨트롤러한테 전달됨 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(); } } // ✅ 옵저버 패턴에서 Subject 역할을 하는 Mode이 호출할 함수를 제공해야 함. /** * 모델 상태가 변경되면 모델의 notify 메서드에서 호출하는 메서드 */ public void updateBPM() { if (model != null) { int bpm = model.getBPM(); if (bpm == 0) { if (bpmOutputLabel != null) { // ✅ 각각의 UI 컴포넌트를 직접 조작 합니다. (컴포지트 패턴) bpmOutputLabel.setText("offline"); } } else { if (bpmOutputLabel != null) { bpmOutputLabel.setText("Current BPM: " + model.getBPM()); } } } } . . . }
마지막으로 Controller 입니다.
Controller도 조건을 만족하는지 봐볼까요?
- 컨트롤러에서는 사용자가 사용할 함수만 제공하고,
- UI 처리는 View에서 처리하도록 합니다. (전략 패턴)
// 뷰에서 컨트롤러에 대해 호출할 모든 인터페이스가 들어있다. 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(); } // 사용자 인터페이스 메뉴에서 Start를 선택 // => 컨트롤러에서는 모델의 on() 메서드를 호출 // => 사용자 인터페이스 메뉴에서 Start 항목을 비활성 상태로, Stop 항목은 활성 상태로 바꿈 public void start() { model.on(); // ✅ UI 처리는 View에서 처리하도록 합니다. (전략 패턴) view.disableStartMenuItem(); view.enableStopMenuItem(); } // 사용자 인터페이스 메뉴에서 Stop을 선택 // Start 선택과 반대 작업 public void stop() { model.off(); view.disableStopMenuItem(); view.enableStartMenuItem(); } // 사용자가 >> 버튼을 클릭 // => 컨트롤러에서는 모델로부터 BPM을 알아내고, 거기에 1을 더한 다음 BPM 값을 새로 설정 public void increaseBPM() { int bpm = model.getBPM(); model.setBPM(bpm + 1); } public void decreaseBPM() { int bpm = model.getBPM(); model.setBPM(bpm - 1); } // 사용자가 임의의 BPM값을 설정하려는 경우 public void setBPM(int bpm) { model.setBPM(bpm); } }
정리
① 사용자는 뷰하고만 접촉할 수 있다 : 뷰에 대해 이벤트가 발생하면 컨트롤러에게 알린다
② 컨트롤러에선 사용자가 행동 할수 있는 함수들이 제공되고, 그 함수 안에선 모델에게 상태변경을 요청한다
③ 컨트롤러에서 뷰를 변경하라고 요청할 수 있다
④ 상태가 변경되면 모델에서 뷰에게 그 사실을 알린다
⑤ 뷰에서 모델에게 상태를 요청한다.
❓ 컨트롤러가 모델의 옵저버가 되는 경우는 없나요?
⇒ 있다. 모델 상태에 의해 사용자가 컨트롤 하는 부분이 바뀐다고 하면 그렇게 해야한다.
MVC 패턴 VS 3-tier Layered Architecture
Layered Architecture란?
프레젠테이션 계층 - 비즈니스 계층 - 데이터 계층을 각각 독립된 모듈로 개발하고 유지하는 구조를 말한다.
- 외부 요청 처리는 가장 바깥 Presentation Layer에서~
- 내부 DB 커넥션 관리, 쿼리 작업은 가장 안쪽 Repositioy Layer에서~
⇒ DB가 mysql에서 mongo로 바뀐다고 해도 Repository 객체의 코드만 바꿔주면 되므로 상위 레이어에 변경이 전파되지 않는다.
❓ 그럼 MVC와 어떤 차이가 있을까?
- 구조적인 차이
Layered Architecture 는 선형 구조 입니다. 상위 계층에서 하위로 가기위해서는 반드시 중간 계층을 거쳐야 합니다. 그러나 MVC패턴은 삼각형 구조로 서로가 서로와 상호작용 합니다.
- 범위 에서의 차이
3계층 애플리케이션은 애플리케이션의 모든 코드를 참조합니다. 그에 반해 MVC 패턴은 UI와 관련된 계층(Presentation, 일부 Business)에서 만 주로 사용 되는 패턴입니다.
MVC X 3-tier Layered Architecture
출처 :
'✏️ 스터디 모음집 > 디자인 패턴 스터디' 카테고리의 다른 글
클린코드 - 깨끗한 테스트 코드 짜기 (0) 2022.11.05 디자인 패턴 스터디 기록 (6) - 싱글톤 패턴 (0) 2022.11.02 디자인 패턴 스터디 기록 (5) - 팩토리 패턴, 의존성 역전 (0) 2022.11.02 디자인 패턴 스터디 기록 (4) - 데코레이터 패턴 (0) 2022.11.02 디자인 패턴 스터디 기록 (3) - 객체 간의 강한 결합 VS 느슨한 결합 (0) 2022.10.27