본문 바로가기
3학년 2학기 학사/소프트웨어 분석 및 설계

[소프트웨어 분석 및 설계] L23. Mediator Pattern, Memento Pattern, Observer Pattern

by whiteTommy 2024. 12. 13.
반응형

Mediator Pattern

  • 중재자 (mediator) 패턴이란?
    • 객체 간의  직접 통신을 제한하고 중재자 객체를 통해서만 협력하도록 하는 행동 패턴
      • 중재자 : 객체 간의 통신을 관리하고 매개체 역할을 함
      • 객체 간의 결합을 낮추고 유연성을 확보
  • 중재자 패턴이 필요한 상황
    • 프로필을 만들고 편집하기 위한 대화상자 (dialog) 에서 다양한 요소들이 상호 작용할 수 있음
      • 각 객체들이 서로 필요한 객체들을 참조
      • 한 클래스가 수정되면 연관된 클래스들의 수정이 발생할 수 있음
  • 중재자 패턴의 아이디어
    • 각 객체 간의 연결을 느슨하게 만들어야 함
      • 객체 간의 직접적인 통신은 중단하고, 이러한 호출 대신 처리할 수 있는 중재제 객체를 두어서 간접적으로 협력
      • M:N 관계를 M:1 로 전환
  • 중재자 패턴의 구조
  • 중재자 패턴의 코드 예시
    public interface Mediator {
        void sendMessage(Component sender, String message); // 메시지 전달 메서드
        void addUser(Component user);                      // 사용자 추가 메서드
    }
    
    public class ConcreteMediator implements Mediator {
        private List<Component> users; // 사용자 목록
    
        public ConcreteMediator() {
            this.users = new ArrayList<>(); // 사용자 목록 초기화
        }
    
        @Override
        public void addUser(Component user) {
            this.users.add(user); // 사용자 추가
        }
    
        @Override
        public void sendMessage(Component sender, String message) {
            for (Component receiver : users) {
                if (sender != receiver) { // 보낸 사람(sender)을 제외하고 메시지 전송
                    receiver.receive(message);
                }
            }
        }
    }
    
    public class Component {
        private Mediator mediator; // Mediator 참조
        private String name;       // 사용자 이름
    
        public Component(Mediator mediator, String name) {
            this.mediator = mediator;
            this.name = name;
        }
    
        public void receive(String message) {
            System.out.println("[수신] > " + this.name + " : " + message); // 메시지 수신 출력
        }
    
        public void send(String message) {
            System.out.println("[발신] > " + this.name + " : " + message); // 메시지 발신 출력
            mediator.sendMessage(this, message); // Mediator에 메시지 전달 요청
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Mediator mediator = new ConcreteMediator(); // ConcreteMediator 생성
    
            // 사용자 생성
            Component user1 = new Component(mediator, "A");
            Component user2 = new Component(mediator, "B");
            Component user3 = new Component(mediator, "C");
    
            // Mediator에 사용자 등록
            mediator.addUser(user1);
            mediator.addUser(user2);
            mediator.addUser(user3);
    
            // 메시지 전송
            user1.send("Hello, World!");
            user2.send("Who are you?");
        }
    }

  • 장점
    • 다양한 컴포넌트간의 통신을 한 곳으로 추출하여 코드를 이해하고 유지관리하기 쉽게 만듬
    • 프로그램의 다양한 컴포넌트 간의 결합도를 줄일 수 있음
    • 개별 컴포넌트들을 더 쉽게 재사용할 수 있음
    • 실제 컴포넌트들을 변경하지 않아도 새로운 중재자를 도입할 수 있음
  • 단점
    • 중재자는 god object 로 발전할 수도 있음
  • 퍼사드 패턴과의 차이점
    • 퍼사드 패턴은 객체들의 하위 시스템에 대한 단순화된 인터페이스를 정의하지만 새로운 기능을 도입하지는 않음
    • 하위 객체가 퍼사드의 존재를 인지하지 못한다.
    • 중재자는 시스템 컴포넌트 간의 통신을 중앙 집중화 함
  • 퍼사드 패턴과 공통점
    • God object 가 될 수 있다. 

 

 

Memento Pattern

  • 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태 값을 저장하고 복원할 수 있게 해주는 행동 패턴
    • 스냅샷이라고도 불림
  • 메멘토 패턴이 필요한 상황
    • 텍스트 편집기에서 실행 취소를 구현하려고 할 때, 특정 작업을 수행하기 전에 모든 객체의 상태를 기록하고 실행 취소하면 기록에서 가장 최신 스냅샷을 가져와 복원 
    • 이를 위해서 객체의 모든 필드를 살펴본 후 해당 값들을 복사해야 함
    • 그러나 대부분의 실제 객체들은 모두 중요한 데이터를 비공해함 (캡슐화)
    • 공개된 필드라도 할 지라도, 객체 일부가 수정되면 복사를 맡은 클래스들 역시 변경이 되어야 함
  • 메멘토 패턴의 아이디어
    • 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 originator 객체에 위임하고 memento 라는 특수 객체에 상태의 복사본을 저장
      • 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 어떤 객체에도 접근할 수 없음
  • 메멘토 패턴의 구조 - 중첩 클래스에 기반
    public class Editor {
        private String text; // 텍스트
        private int posX, posY; // 커서 위치
    
        // 현재 상태를 저장하는 Snapshot 생성
        public Snapshot create() {
            System.out.println("Create");
            return new Snapshot();
        }
    
        // 이전 상태로 복원
        public void restore(Snapshot snapshot) {
            System.out.println("Restore");
            this.text = snapshot.getText();
            this.posX = snapshot.getPosX();
            this.posY = snapshot.getPosY();
        }
    
        // getters and setters
    
        // Memento 내부 클래스
        final class Snapshot {
            final String text;
            final int posX, posY;
    
            // 현재 상태를 Snapshot으로 저장
            Snapshot(String text, int posX, int posY) {
                this.text = text;
                this.posX = posX;
                this.posY = posY;
            }
    
            // getters
        }
    }
    
    public class Command {
        private Stack<Editor.Snapshot> stack; // 상태를 저장하는 스택
    
        public Command() {
            this.stack = new Stack<>();
        }
    
        // 상태 백업
        public void makeBackup(Editor origin) {
            Editor.Snapshot memento = origin.create();
            stack.push(memento);
        }
    
        // 상태 복원
        public Editor undo(Editor editor) {
            if (!stack.isEmpty()) {
                Editor.Snapshot memento = stack.pop(); // 최근 상태 가져오기
                editor.restore(memento); // 복원
            }
            return editor;
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            Editor editor = new Editor();
            Command care = new Command();
    
            // 상태 1 저장
            editor.setText("design pattern 1 - Memento");
            editor.setPos(10, 24);
            System.out.println(editor.getState());
            care.makeBackup(editor);
    
            // 상태 2 저장
            editor.setText("design pattern 2 - Iterator");
            editor.setPos(22, 24);
            System.out.println(editor.getState());
            care.makeBackup(editor);
    
            // 상태 3 저장
            editor.setText("design pattern 3 - Prototype");
            editor.setPos(44, 24);
            System.out.println(editor.getState());
            care.makeBackup(editor);
    
            // 상태 복원
            System.out.println("\n=== 복구 ===\n");
            Editor obj = care.undo(editor); // 상태 3 -> 상태 2
            System.out.println(obj.getState());
    
            obj = care.undo(editor); // 상태 2 -> 상태 1
            System.out.println(obj.getState());
    
            obj = care.undo(editor); // 상태 1 (더 이상 복구할 상태 없음)
            System.out.println(obj.getState());
        }
    }


  • 장점
    • 캡슐화를 위반하지 않고 객체의 상태 스냅샷을 생성할 수 있음
    • 케어테이커가 오리지네이터의 상태 기록을 유지하도록 하여 오리지네이터의 코드를 단순화할 수 있음
  • 단점
    • SRP 위배
    • 클라이언트의 메멘토를 너무 자주 생성하면 메모리 사용이 늘어남
    • 케어테이커에 오래된 메멘토는 삭제하고 관리하는 역할이 부여될 수 있음

 

 

Observer Pattern

  • 옵저버 패턴이란?
    • 옵저버들이 관찰하고 있는 대상의 상태 변화가 있을 때마다 대상자는 각 관찰자에게 통지하고 관찰자들은 알림을 받아 조치를 취하는 행동 패턴
      • 발행-구독 모델이라고도 함
  • 옵저버 패턴의 구조
  • 옵저버 패턴의 흐름
    • 한 개의 관찰 대상자와 여러 개의 관찰자로 1:N 관계로 구성
    • 관찰 대상자의 상태가 바뀌면 변경사항을 관찰자에게 통보
    • 통보를 받은 관찰자는 적절하게 필요에 맞추어 업데이트
    • 언제든지 구독 추가/취소가 가능
  • 옵저버 패턴 코드 예시 - 날씨 API 로부터 온습도 알림받기
    interface Subscriber {
        void display(WeatherAPI api);
    }
    
    class KoreanUser implements Subscriber {
        String name;
    
        KoreanUser(String name) {
            this.name = name;
        }
    
        @Override
        public void display(WeatherAPI api) {
            System.out.printf("%s님의 현재 날씨 상태를 조회함 : %.2f°C %.2fg/m³ %.2fhPa\n",
                name, api.temp, api.humidity, api.pressure);
        }
    }
    
    interface Publisher {
        void register(Subscriber subscriber); // 구독자 등록
        void remove(Subscriber subscriber);   // 구독자 제거
        void notify();                        // 구독자들에게 알림
    }
    
    class WeatherAPI implements Publisher {
        float temp;      // 온도
        float humidity;  // 습도
        float pressure;  // 기압
    
        List<Subscriber> subscribers = new ArrayList<>(); // 구독자 리스트
    
        @Override
        public void register(Subscriber subscriber) {
            subscribers.add(subscriber); // 구독자 등록
        }
    
        @Override
        public void remove(Subscriber subscriber) {
            subscribers.remove(subscriber); // 구독자 제거
        }
    
        @Override
        public void notify() {
            for (Subscriber subscriber : subscribers) {
                subscriber.display(this); // 모든 구독자에게 상태 알림
            }
        }
    
        void measurementsChanged() {
            // 랜덤 데이터로 날씨 상태 변경
            temp = new Random().nextFloat() * 100;
            humidity = new Random().nextFloat() * 100;
            pressure = new Random().nextFloat() * 100;
    
            notify(); // 상태가 변경되었음을 알림
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            WeatherAPI api = new WeatherAPI(); // WeatherAPI(Publisher) 생성
    
            // 구독자(Subscriber) 생성 및 등록
            api.register(new KoreanUser("홍길동"));
            api.register(new KoreanUser("임꺽정"));
            api.register(new KoreanUser("세종대왕"));
    
            // 상태 변경 및 알림 전송
            api.measurementsChanged(); // 구독자들에게 새로운 날씨 데이터 전달
        }
    }
  • 장점
    • 관찰 대상자의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지
    • Publisher 의 코드를 변경하지 않고도 새 구독자 클래스를 도입
    • 런타임 시점에 발행자와 구독 알림 관계를 맺음
    • 상태 변경하는 객체와 변경을 감지하는 객체의 관계를 느슨하게 유지
  • 단점
    • 구독자는 알림 순서를 제어할 수 없고, 무작위로 통보만 받음
    • 다수의 observer 객체 등록 이후, 사용하지 않는 observer 를 해지하지 않는다면 memory 낭비가 발생

 

 

 

 

 

 

반응형