ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디자인 패턴 스터디 기록 (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란?

    이미지 출처 : 인프런 NodeJS 밋업 발표자료&nbsp;https://www.inflearn.com/pages/inflearn-evening-meetup-2211

    프레젠테이션 계층 - 비즈니스 계층 - 데이터 계층을 각각 독립된 모듈로 개발하고 유지하는 구조를 말한다.

    • 외부 요청 처리는 가장 바깥 Presentation Layer에서~
    • 내부 DB 커넥션 관리, 쿼리 작업은 가장 안쪽 Repositioy Layer에서~

    ⇒ DB가 mysql에서 mongo로 바뀐다고 해도 Repository 객체의 코드만 바꿔주면 되므로 상위 레이어에 변경이 전파되지 않는다.

     

    ❓ 그럼 MVC와 어떤 차이가 있을까?

     

    1. 구조적인 차이

    Layered Architecture 는 선형 구조 입니다. 상위 계층에서 하위로 가기위해서는 반드시 중간 계층을 거쳐야 합니다. 그러나 MVC패턴은 삼각형 구조로 서로가 서로와 상호작용 합니다.

    1. 범위 에서의 차이

    3계층 애플리케이션은 애플리케이션의 모든 코드를 참조합니다. 그에 반해 MVC 패턴은 UI와 관련된 계층(Presentation, 일부 Business)에서 만 주로 사용 되는 패턴입니다.

    MVC X 3-tier Layered Architecture

     

     

    출처 : 

     

     

    3-tier Layered Architecture와 MVC

    많이 헷갈려하는 3-tier 계층과 MVC의 관계

    velog.io

     

     

    Head First Design Patterns : (12)컴파운드 패턴

    컴파운드 패턴 두 개 이상의 패턴을 결합해 일반적으로 자주 등장하는 문제들에 대한 해법을 제공 SimUDuck 1장에서 사용했던 예제를 처음부터 다시 만들어보자 (bb-dochi.tistory.com/72) 1. Quackable 인터

    bb-dochi.tistory.com

     

    [Compound Pattern] MVC(Model-View-Controller)

    일련의 디자인 패턴들을 함께 사용하여 다양한 디자인 문제를 해결하는 것을 컴파운드 패턴이라고 부른다. 대표적인 컴파운드 패턴 중 하나가 바로 MVC(Model-View-Controller)이다.MP3 재생 소프트웨어

    velog.io

     

    댓글

GitHub: https://github.com/Yesung-Han