Java

[Java] 옵저버 패턴(Observer Pattern)

bkuk 2023. 5. 11. 00:43

옵저버 패턴(Observer Pattern)

옵저버 패턴

옵저버 패턴이란?

객체의 상태 변화를 관찰하는 관찰자들(Observers)의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 각 옵저버에게 통지하도록 하는 디자인 패턴입니다.

하나의 Subject에 여러 Observer등록(register)해 두고, 통지(notify)를 하게 되면, 루프(for observer ..)를 순회하면서 각 ObserverUpdate하는 패턴입니다.

Observer 인터페이스

ObserverSubject에 생긴 변화에 관심을 갖는다.

public interface Observer {
    public void update(Subject theChangedSubject);
}

  • SubjectObserver들을 알고 있는 객체이다.
    • 여러 Observer가 Subject에 붙을 수 있다.

Subject 인터페이스

public interface Subject {
    public void register(Observer o);
    public void unregister(Observer o);
    public void notify();
}

  • register: Subject에 Observer를 등록한다.
  • unregister: Subject에 등록한 Observer의 구독을 해지한다.
  • notify: Subject에서 모든 Observer에 정보를 전달한다.


옵저버 패턴 사용 예제

요구사항: 주식 시장에서는 주가 상승과 주가 하락에 변동이 있을 시 구독자들에게 해당 알림을 통지하도록 한다.


StockMarket Class: Observable 클래스를 상속한다.

  • setChanged() 메서드를 호출한 다음 notifyObservers() 메서드를 호출하여 옵저버에게 주가 변화를 알린다.
    • observable 객체가 변경된 것으로 표시하는 데 사용된다.
    • notifyObservers()에 대한 후속 호출이 실제로 옵저버에게 알릴 것임을 의미한다.

Public class StockMarket extends Observable {

    public void priceRise() {
        System.out.println("Stock price has risen");
        setChanged();
        notifyObservers("Price has risen");
    }

    public void priceFall() {
        System.out.println("Stock price has gone down");
        setChanged();
        notifyObservers("The price has gone down");
    }
}

Subscriber Class: update() 메서드 하나만 있는 Observer 인터페이스를 구현한다.

  • update() 메서드는 옵저버와 공유할 새로운 정보가 있을 때 Observable 클래스(이 경우 StockMarket의 슈퍼클래스)에 의해 자동으로 호출된다.

public class Subscriber implements Observer {
    private String name;

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

    @Override
    public void update(Observable o, Object arg) {
        String message = (String) arg;
        System.out.println(name + "received. " + message);
    }
}


Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다.

이로인해, 자체적으로 인터페이스 및 클래스를 사용해서 옵저버 패턴을 구현하는 것도 좋은 방법이다.

아래 클래스 다이어그램을 보면서 자체적으로 어떻게 구현했는지 보자.


대기 줄 정보를 디스플레이(Observer)가 구독하는 구조로 이루어져 있다.

클래스 다이어그램


대략적인 update() 호출까지의 흐름을 그림으로 살펴보자.

line의 변경사항이 등록되면, update() 메서드를 호출한다.

호출 1


등록된 Observer 목록을 전체 순회하면서 각각의 Observer의 update() 메서드를 호출한다.

호출 2


update() 메서드는 display() 메서드를 호출하게 되며, 모든 Observer는 수신된 콘텐츠를 출력한다.

호출 3


전체적인 흐름은 이해했으리라 생각된다.

이제 구현한 코드를 보면서 깊게 이해해보자.

먼저 Observer와 Subject 인터페이스를 보자.

  • update() 메소드의 인자로 Subject가 아니라 각 값을 전달한다는 점이 다르다.
  • 더욱 느슨한 결합을 선호하며, 전달해야 할 값이 적다면 해당 방법으로 설계해도 괜찮다고 생각한다.

public interface Observer {
    void update( int currentNumber, int totalNumber);
}

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

다음은 Subject의 구현체이다.

변경이 발생할 때, Subject에서 알림을 호출한다.

public class LineData implements Subject {
    private List<Observer> observers;
    private int currentNumber;
    private int totalNumber;

    public LineData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObservers() {
        for( Observer observer : observers ) {
            observer.update(currentNumber, totalNumber);
        }
    }

    public void lineChanged() {
        notifyObservers();
    }

    public void setLine(int currentNumber, int totalNumber) {
        this.currentNumber = currentNumber;
        this.totalNumber = totalNumber;
        lineChanged();  // 변경이 발생할 때, 알림을 돌리는 방법 선택
    }
}

Display를 위한 인터페이스이다.

public interface DisplayElement {
    void display();
}

그리고 Observer 구현체.

update() 메서드가 호출될 때 마다, display() 메서드가 호출되어 화면을 출력하도록 설정되어있다.

생성자를 통해 파라미터로 받은 Subject에 자기 자신인 this을 등록하기 때문에 main() 메소드에서 Subject에 옵저버를 일일이 등록하지 않는다.

public class CurrentLineCondition implements DisplayElement, Observer {
    private int myNumber;
    private int currentNumber;
    private int totalNumber;
    private Subject lineData;

    public CurrentLineCondition(int myNumber, Subject turnData) {
        this.myNumber = myNumber;
        this.lineData = turnData;
        turnData.registerObserver(this);
    }

    @Override
    public void update(int waitingNumber, int totalNumber) {
        this.currentNumber = waitingNumber;
        this.totalNumber = totalNumber;
        display();
    }

    @Override
    public void display() {
        System.out.println(
                String.format("나의 순번: %d, 대기 순번: %d번, 총 순번: %d번",
                        myNumber, currentNumber, totalNumber));
    }
}

테스트를 위해 작성한 main 메소드이다.

public class Application {
    public static void main(String[] args) {
        LineData line = new LineData();
        CurrentLineCondition current1 = new CurrentLineCondition(30, line);
        CurrentLineCondition current2 = new CurrentLineCondition(40, line);
        CurrentLineCondition current3 = new CurrentLineCondition(50, line);

        line.setLine(20, 100);
        line.setLine(21, 100);
        line.setLine(22, 100);
    }
}