[GoF 디자인 패턴] 메멘토(Memento) 패턴, 옵저버 (Observer) 패턴

JMM·2025년 1월 14일
0

GoF 디자인 패턴

목록 보기
9/11
post-thumbnail

1. 메멘토(Memento) 패턴 : 캡슐화를 유지하면서 객체 내부 상태를 외부에 저장하는 방법.

객체 상태를 외부에 저장했다가 해당 상태로 다시 복구할 수 있다.


Before

코드

Client

public class Client {

    public static void main(String[] args) {
        Game game = new Game();
        game.setRedTeamScore(10);
        game.setBlueTeamScore(20);

        // 상태를 클라이언트가 직접 저장
        int blueTeamScore = game.getBlueTeamScore();
        int redTeamScore = game.getRedTeamScore();

        // 상태를 클라이언트가 직접 복구
        Game restoredGame = new Game();
        restoredGame.setBlueTeamScore(blueTeamScore);
        restoredGame.setRedTeamScore(redTeamScore);
    }
}

Game

public class Game implements Serializable {

    private int redTeamScore;
    private int blueTeamScore;

    public int getRedTeamScore() {
        return redTeamScore;
    }

    public void setRedTeamScore(int redTeamScore) {
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public void setBlueTeamScore(int blueTeamScore) {
        this.blueTeamScore = blueTeamScore;
    }
}

구조 및 문제점

  1. Game 객체의 상태 복구
  • 클라이언트가 직접 Game 객체의 내부 상태(스코어)를 가져와 복사한 후 새로운 객체에 설정.
  • 저장과 복구 로직이 클라이언트에 있어 캡슐화가 깨짐.
  1. 문제점
  • 클라이언트가 객체의 상태를 직접 관리하므로 응집도가 낮아지고 유지보수성이 떨어짐.
  • 새로운 상태가 추가될 경우, 클라이언트 코드에도 변경이 필요.

After

개선 사항

  1. GameSave 클래스 도입:

    • Game 객체의 상태를 저장하고 복구할 수 있는 불변 객체(GameSave) 도입.
    • 캡슐화를 유지하면서 상태 저장과 복구를 Game 내부에서 처리.
  2. save()restore() 메서드 추가:

    • Game.save(): 현재 상태를 GameSave 객체로 저장.
    • Game.restore(): GameSave 객체를 기반으로 상태 복구.

코드 분석

GameSave

public final class GameSave {

    private final int blueTeamScore; // 블루 팀 스코어
    private final int redTeamScore;  // 레드 팀 스코어

    public GameSave(int blueTeamScore, int redTeamScore) {
        this.blueTeamScore = blueTeamScore;
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public int getRedTeamScore() {
        return redTeamScore;
    }
}

Game

public class Game {

    private int redTeamScore;
    private int blueTeamScore;

    public int getRedTeamScore() {
        return redTeamScore;
    }

    public void setRedTeamScore(int redTeamScore) {
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public void setBlueTeamScore(int blueTeamScore) {
        this.blueTeamScore = blueTeamScore;
    }

    // 현재 상태를 저장
    public GameSave save() {
        return new GameSave(this.blueTeamScore, this.redTeamScore);
    }

    // 이전 상태로 복구
    public void restore(GameSave gameSave) {
        this.blueTeamScore = gameSave.getBlueTeamScore();
        this.redTeamScore = gameSave.getRedTeamScore();
    }
}

Client

public class Client {

    public static void main(String[] args) {
        Game game = new Game();
        game.setBlueTeamScore(10);
        game.setRedTeamScore(20);

        // 현재 상태 저장
        GameSave save = game.save();

        // 상태 변경
        game.setBlueTeamScore(12);
        game.setRedTeamScore(22);

        // 이전 상태로 복구
        game.restore(save);

        // 출력: 복구된 상태
        System.out.println(game.getBlueTeamScore()); // 10
        System.out.println(game.getRedTeamScore()); // 20
    }
}

이러면 새로운 객체를 생성하지 않고도 상태를 복구할 수 있다!


동작 과정

  1. 현재 상태 저장:

    • Game.save() 메서드를 호출하여 현재 상태를 GameSave 객체로 캡슐화.
  2. 상태 복구:

    • Game.restore(GameSave) 메서드를 호출하여 GameSave 객체를 기반으로 상태 복구.
  3. 상태 변경과 복구:

    • Game의 상태를 마음대로 변경한 후, 저장된 상태로 되돌릴 수 있음.

다이어그램


구성 요소

  1. Originator (기원자):

    • 상태를 보유하고, 이를 저장(save())하거나 복구(restore())하는 객체.
    • 예제에서는 Game 클래스가 Originator 역할을 한다.
  2. Memento (메멘토):

    • Originator 객체의 상태를 캡슐화하고 저장하는 객체.
    • 예제에서는 GameSave 클래스가 Memento 역할을 한다.
  3. Caretaker (관리자):

    • Memento 객체를 관리하며, 저장된 상태를 복원할 때 Originator에 전달.
    • 예제에서 Client 클래스가 Caretaker 역할을 한다.

Caretaker는 Memento를 보관하고 필요할 때 이를 Originator로 전달하여 상태 복구를 요청한다 !


메멘토 패턴의 장단점

장점

  1. 캡슐화를 지키면서 상태 저장

    • 객체의 내부 상태를 노출하지 않고도 상태 스냅샷을 생성할 수 있다.
    • 객체의 필드에 직접 접근하지 않고, save()restore() 메서드를 통해 상태를 관리.
  2. 상태 관리의 책임 분리

    • 객체 상태를 저장하고 복원하는 책임을 Caretaker가 담당하므로, 객체의 책임이 단순화된다.
  3. 클라이언트 코드의 독립성

    • 객체의 상태가 변경되더라도 클라이언트 코드에는 영향을 미치지 않는다.
    • 저장 및 복원은 MementoOriginator 내부에서 처리.

단점

  1. 메모리 사용량 증가

    • 많은 상태를 저장하거나, Memento 객체를 자주 생성하는 경우 메모리 사용량이 증가할 수 있다.
    • 특히 상태가 크거나 빈번히 저장/복원해야 한다면 성능에 영향을 줄 수 있음.
  2. 복잡성 증가

    • 상태 저장 및 복구를 위한 추가 클래스(Memento, Caretaker)와 로직이 필요하므로, 코드 복잡도가 증가할 수 있다.

메멘토 패턴, 실무에서는?

자바

1) 객체 직렬화, java.io.Serializable

public class MementoInJava {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // TODO Serializable
        Game game = new Game();
        game.setRedTeamScore(10);
        game.setBlueTeamScore(20);

        // TODO 직렬화
        try(FileOutputStream fileOut = new FileOutputStream("GameSave.hex");
        ObjectOutputStream out = new ObjectOutputStream(fileOut))
        {
            out.writeObject(game);
        }

        game.setBlueTeamScore(25);
        game.setRedTeamScore(15);

        // TODO 역직렬화
        try(FileInputStream fileIn = new FileInputStream("GameSave.hex");
            ObjectInputStream in = new ObjectInputStream(fileIn))
        {
            game = (Game) in.readObject();
            System.out.println(game.getBlueTeamScore());
            System.out.println(game.getRedTeamScore());
        }
    }
}

2) java.util.Date


2. 옵저버 (Observer) 패턴 : 다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴.

1) 발행(publish)-구독(subscribe) 패턴을 구현할 수 있다.
2) 상태 변화를 자동으로 알림으로써 클라이언트 간 결합도를 낮추고 확장성을 높인다.


Before

ChatServer

  • 단순히 주제별 메시지를 저장하고 요청에 따라 반환하는 역할.
  • 발행 및 구독 개념이 없음.
public class ChatServer {

    private Map<String, List<String>> messages;

    public ChatServer() {
        this.messages = new HashMap<>();
    }

    public void add(String subject, String message) {
        if (messages.containsKey(subject)) {
            messages.get(subject).add(message);
        } else {
            List<String> messageList = new ArrayList<>();
            messageList.add(message);
            messages.put(subject, messageList);
        }
    }

    public List<String> getMessage(String subject) {
        return messages.get(subject);
    }
}

User

  • ChatServer를 직접 호출하여 메시지를 저장하거나 가져옴.
  • 발행 및 구독과 무관한 단순 사용자 역할.
public class User {

    private ChatServer chatServer;

    public User(ChatServer chatServer) {
        this.chatServer = chatServer;
    }

    public void sendMessage(String subject, String message) {
        chatServer.add(subject, message);
    }

    public List<String> getMessage(String subject) {
        return chatServer.getMessage(subject);
    }
}

Client

  • 메시지를 보내고 수동으로 요청해서 읽는 구조.
  • 상태 변화에 따른 알림 기능 없음.
public class Client {

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();

        User user1 = new User(chatServer);
        user1.sendMessage("디자인패턴", "옵저버 패턴입니다.");
        user1.sendMessage("롤드컵2021", "LCK 화이팅!");

        User user2 = new User(chatServer);
        System.out.println(user2.getMessage("디자인패턴")); // 수동 요청

        user1.sendMessage("디자인패턴", "코드 분석 중...");
        System.out.println(user2.getMessage("디자인패턴")); // 다시 요청
    }
}

Before 문제점

  1. 구독-발행 구조 부재:

    • 상태 변화가 발생해도 구독자가 이를 자동으로 알림받을 수 없다.
    • 사용자가 직접 요청해야 메시지를 확인할 수 있다.
  2. 결합도 높음:

    • UserChatServer가 강하게 결합되어 있어 변경 및 확장이 어렵다.
  3. 확장성 부족:

    • 새로운 기능(예: 메시지 필터링, 조건부 구독)을 추가하려면 기존 코드를 대규모로 수정해야 한다.
  4. 알림 비효율성:

    • 상태 변화에 따른 알림 자동화가 없고, 상태 확인은 모두 수동으로 처리된다.

After

수정 내용

  1. 구독-발행 구조 도입:

    • ChatServer는 발행자(Subject) 역할, User는 구독자(Observer) 역할을 수행한다.
    • 주제를 구독하고 상태가 변하면 자동으로 알림을 받을 수 있다.
  2. 결합도 감소:

    • Subscriber 인터페이스를 통해 UserChatServer 간의 강한 결합을 제거했다.
  3. 확장성 향상:

    • 새로운 기능 추가(조건부 알림, 메시지 필터링 등)가 기존 코드 변경 없이 가능해졌다.
  4. 알림 자동화:

    • 상태 변화가 발생하면 구독자에게 자동으로 알림이 전달된다.

ChatServer

  • 발행자 역할로, 주제를 구독한 모든 구독자에게 상태 변화를 알린다.
  • 구독자 등록(register), 구독 취소(unregister), 알림 전송(sendMessage) 기능을 제공한다.
public class ChatServer {

    private Map<String, List<Subscriber>> subscribers = new HashMap<>();

    public void register(String subject, Subscriber subscriber) {
        subscribers.computeIfAbsent(subject, k -> new ArrayList<>()).add(subscriber);
    }

    public void unregister(String subject, Subscriber subscriber) {
        if (subscribers.containsKey(subject)) {
            subscribers.get(subject).remove(subscriber);
        }
    }

    public void sendMessage(User user, String subject, String message) {
        if (subscribers.containsKey(subject)) {
            String userMessage = user.getName() + ": " + message;
            subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
        }
    }
}

Subscriber

  • 구독자 인터페이스로, 알림을 처리하는 handleMessage 메서드를 제공한다.
public interface Subscriber {
    void handleMessage(String message);
}

User

  • Subscriber 인터페이스를 구현하여 알림을 받을 수 있는 구독자로 동작.
  • 알림을 받을 때 메시지를 출력한다.
public class User implements Subscriber {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void handleMessage(String message) {
        System.out.println(message);
    }
}

Client

  • 구독 및 메시지 발행의 주요 흐름을 테스트.
  • 구독자는 특정 주제를 구독하고 상태 변화에 따라 자동으로 알림을 받는다.
public class Client {

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        User user1 = new User("keesun");
        User user2 = new User("whiteship");

        chatServer.register("디자인패턴", user1);
        chatServer.register("디자인패턴", user2);

        chatServer.sendMessage(user1, "디자인패턴", "옵저버 패턴 구현 완료!");
    }
}

구성요소

  • Subject (발행자): 상태 변화를 관리하며, 구독자들에게 알림을 보냄.

    • 예제에서 ChatServer가 해당한다.
    • 주제를 관리하고, 구독자(Observer)를 등록/해제하며 메시지를 발행하면 구독자들에게 알림을 보낸다.
  • Observer (구독자): 발행자의 상태 변화를 감지하고 처리.

    • 예제에서 User가 해당한다.
    • Subscriber 인터페이스를 구현하여 알림을 받을 준비가 되어 있는 객체이다.
  • ConcreteObserver (구체적인 구독자): 실제 알림을 처리하는 Observer의 구체적인 구현체.

    • 예제에서 User 자체가 ConcreteObserver이다.
    • Subscriber 인터페이스를 구현하고, 알림이 올 때 메시지를 출력하는 기능을 구현하였다.

옵저버 패턴 장단점

장점

상태를 변경하는 객체(publisher)변경을 감지하는 객체(subsriber)의 관계를 느슨하게 유지할 수 있다.
Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
런타임에 옵저버를 추가하거나 제거할 수 있다.

단점

• 복잡도가 증가한다.
• 다수의 Observer 객체를 등록 이후 해지 않는다면 memory leak이 발생할 수도 있다.


옵저버 패턴, 실무에서는?

1) Java

1-1. PropertyChangeListener

  • 개요: PropertyChangeListenerPropertyChangeSupport를 활용해 상태 변화를 감지하고 구독자에게 알림.
  • 특징: 간단한 옵저버 패턴 구현에 적합.
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class PropertyChangeExample {

    static class User implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println(evt.getNewValue());
        }
    }

    static class Subject {
        PropertyChangeSupport support = new PropertyChangeSupport(this);

        public void addObserver(PropertyChangeListener observer) {
            support.addPropertyChangeListener(observer);
        }

        public void removeObserver(PropertyChangeListener observer) {
            support.removePropertyChangeListener(observer);
        }

        public void add(String message) {
            support.firePropertyChange("eventName", null, message);
        }
    }

    public static void main(String[] args) {
        Subject subject = new Subject();
        User observer = new User();

        subject.addObserver(observer);
        subject.add("자바 PCL 예제 코드");

        subject.removeObserver(observer);
        subject.add("이 메시지는 볼 수 없지..");
    }
}

1-2. Flow API

  • 개요: 자바 9에서 도입된 비동기 데이터 처리 API.
  • 특징: 대량 데이터 처리 시 적합하며, PublisherSubscriber로 발행-구독 구현.
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class FlowInJava {

    public static void main(String[] args) throws InterruptedException {
        Flow.Publisher<String> publisher = new SubmissionPublisher<>();

        Flow.Subscriber<String> subscriber = new Flow.Subscriber<String>() {
            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                System.out.println("Subscribed!");
                this.subscription = subscription;
                this.subscription.request(1); // 데이터 요청
            }

            @Override
            public void onNext(String item) {
                System.out.println("Received: " + item);
                subscription.request(1); // 다음 데이터 요청
            }

            @Override
            public void onError(Throwable throwable) {
                System.err.println("Error occurred: " + throwable.getMessage());
            }

            @Override
            public void onComplete() {
                System.out.println("All items processed");
            }
        };

        publisher.subscribe(subscriber);
        ((SubmissionPublisher<String>) publisher).submit("Hello, Java Flow!");
        System.out.println("Publisher 작업 완료");
    }
}

2) Spring

2-1. ApplicationEvent와 ApplicationEventPublisher

  • 개요: 스프링에서 제공하는 이벤트 시스템으로, 발행-구독 모델을 간단히 구현.
  • 특징: 이벤트 리스너와 퍼블리셔를 통해 느슨한 결합 유지.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

public class MyEvent {

    private String message;

    public MyEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

@Component
public class MyEventListener {
    @EventListener(MyEvent.class)
    public void onApplicationEvent(MyEvent event) {
        System.out.println(event.getMessage());
    }
}

@Component
public class MyRunner implements ApplicationRunner {
    private final ApplicationEventPublisher publisher;

    public MyRunner(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        publisher.publishEvent(new MyEvent("Hello Spring Event!"));
    }
}

@SpringBootApplication
public class ObserverInSpring {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(ObserverInSpring.class);
        app.setWebApplicationType(WebApplicationType.NONE);
        app.run(args);
    }
}

출처 : 코딩으로 학습하는 GoF의 디자인 패턴

0개의 댓글