2. 옵저버 패턴(Observer Pattern)

Kim Dong Kyun·2023년 6월 22일
1

Design Pattern

목록 보기
2/5

썸네일 링크

옵저버 패턴이란?

"객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 구성하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴"

즉, 한 객체의 변화에 대한 "리스너"들을(실제 자바/스프링 구현 환경에서도 ~Listener로 네이밍된 클래스들을 만날 수 있을 것이다) 변화의 주체가되는 객체 안에 구성하는 것이다.

매우 매우 정형화된 패턴이 있으며, 자바에서 Library로 등록 된 적도 있을 정도로 패턴 사용에 대한 유연성이랄 게 잘 없다. (다만, 현재는 Deprecated. 어쨋든 디자인 패턴은 직접 사용,구현하여 유연성과 그 장점을 최대로 챙기기 위함인 듯)

그럼, 정형화된 패턴을 한번 알아보자.


1. Subject

public interface Subject {
    void registerObserver(Observer observer);

    void removeObserver(Observer observer);

    void notifyObserver();
}
  • "주제" 가 되는, 실제 변경이 일어나는 객체의 추상이다.

  • Subject를 상속하는 실제 구현체들은 3 개의 매서드를 오버라이드 받는 것을 강제받고 있으며, 이는 각각 다음과 같은 기능을 한다.

  1. registerObserver() : 옵저버를 등록한다. ( 관리하며, 상태 변경이 일어나도록 한다)
  2. removeObserver() : 옵저버를 등록 해제한다 ( 더 이상 관리하지 않으며, 상태 변경도 일으키지 않는다 )
  3. notifyObserver() : Subject 객체의 변화를 전파한다. ( 연관된 옵저버들을 일제히 상태 변경 시킨다)

2. Observer

public interface Observer {
    void update();
}
  • "옵저버"이자 "Listener". 즉, Subject 객체의 변화에 따라 .update()매서드를 호출하고, 그에 따른 상태 변경이 일어나도록 강제된다.

오케이, 알겠는데 이걸 어따 써먹나요?


예시를 들어보자 (기상청 API)

예시 v1 - push

예를 들어 기상청의 날씨 API를 적절한 곳에 뿌려줘야 하는 상황이 있다고 해보자. 그렇다면 다음과 같은 구현이 가능하다.

public class WeatherData implements Subject {

    private Float temperature; // 기온
    private Float pressure; // 기압
    private List<Observer> observers;

    public WeatherData() {
        observers = new ArrayList<>();
    }

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

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObserver() {
        for (Observer observer : observers) {
            observer.update(temperature, pressure);
        }
    }
    
       public void setStatus(Float temperature, Float pressure){
        this.temperature = temperature;
        this.pressure = pressure;
        notifyObserver();
    }
}
  • 옵저버의 리스트를 "구성" 요소로 가진다.

  • 오버라이드한 매서드를 통해 모든 옵저버들을 업데이트 한다. (기온, 기압)

  • 언뜻 보기엔 괜찮아 보인다. 한번 테스트 해 볼까?


1. Test 1

public class CurrentWeather implements Observer {

    private Float temperature; // 기온
    private Float pressure; // 기압

    @Override
    public void update(Float temperature, Float pressure) {
        this.pressure = pressure;
        this.temperature = temperature;
        display();
    }
    
    public void display(){
        System.out.println("업데이트 되었음을 알립니다");
    }
}
  • Observer 추상의 구현체인 "현재 날씨"

  • update 매서드를 통해서 현재 날씨 정보를 갱신한다

  • 갱신 후, 업데이트 된 것을 표현하기 위해 display() 매서드를 사용했다.

public class ObserverPatternTest {
    public static void main(String[] args) {

        Subject weatherData = new WeatherData(); 
        // 추상 타입으로 구현체 인스턴스 생성

        Observer currentWeather = new CurrentWeather();
        // 마찬가지

        weatherData.registerObserver(currentWeather);
        // currentWeather를 옵저버 목록에 추가함

        weatherData.notifyObserver();
        // 옵저버 목록의 모든 녀석들을 업데이트함!
    }
}

  • 의도한대로 동작하고 있다.

하지만, 실시간 기상 데이터가 WeatherData에 꽂히고 있고, 구현체가 굉장히 많아서 구현체마다 이 데이터를 받아야 하는 시간이 각기 다르다면 어떨까?

  • 대부분의 구현체가 원하지 않는 시간에 .update() 매서드를 호출하게 될 것이고, 이는 낭비이다.

예시 v2 - pull

그렇다면, Subject에서 변경을 push하는 것이 아니라 구현체마다 필요 할 때 pull 받는 게 낫지 않을까?

  • 위 코드를 수정해보자

1. Subject - WeatherData

public class WeatherData implements Subject {

    private Float temperature; // 기온
    private Float pressure; // 기압
    private List<Observer> observers;

    public Float getTemperature() {
        return temperature;
        // getter 매서드 추가
    }

    public Float getPressure() {
        return pressure;
        // getter 매서드 추가
    }

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

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObserver() {
        for (Observer observer : observers) {
            observer.update();
        }
    }
}
  • 위와 같이 Getter 매서드를 추가해주고, obsever.update의 패러미터로는 Subject를 설정한다. (하위 추상인 해당객체 this를 아규먼트로 사용)

2. Observer

public interface Observer {
    void update(Subject subject);
}
public class CurrentWeather implements Observer {

    private Float temperature; // 기온
    private Float pressure; // 기압

    @Override
    public void update(Subject subject) {
        if (subject instanceof WeatherData){
            this.temperature = ((WeatherData) subject).getTemperature();
            this.pressure = ((WeatherData) subject).getPressure();
            display();
        }
    }

    public void display(){
        System.out.println("업데이트 되었음을 알립니다");
    }
}
  • update() 매서드 부분을 다르게 변경했는데, 아규먼트로 들어온 subject 가 WeatherData의 인스턴스임을 확인 한 후에, 해당 옵저버의 상태 (기온, 기압)를 변경시키고, 업데이트를 알림한다.

Test 2

WeatherData 에 세터 매서드를 추가해서 실제 값을 바꿔 보고, 테스트 해보자

추가적으로 display() 매서드도 기온을 표시할 수 있게 해주자.

public class CurrentWeather implements Observer {

    private Float temperature; // 기온
    private Float pressure; // 기압

    @Override
    public void update(Subject subject) {
        if (subject instanceof WeatherData){
            this.temperature = ((WeatherData) subject).getTemperature();
            this.pressure = ((WeatherData) subject).getPressure();
            display(temperature, pressure);
        }
    }

    public void display(Float temperature, Float pressure){
        System.out.println("기온은 (" + temperature + "), 기압은 (" + pressure + ") 입니다.");
    }
}
  • 위는 디스플레이 추가한 모습
  • display 매서드는 update 매서드 호출시에만 사용된다

위와 같이, pull 방식으로 Observer 스스로가 필요 할 때 데이터를 불러오고 사용 가능하다. (이 방식이 더 안전하다!)


의문

이거 완전 구독 아니냐? Pub/Sub 패턴 이거랑 똑같으니까 패턴 하나 날먹했누 ㅋㅋ

  • 아니라고 한다. 링크

  • PUB/SUB 패턴은 중간에 메시지 브로커, 이벤트 버스가 존재한다 이 이유 때문에 아래 모든 장점을 가진다.

  • 따라서 "결합도가 더 낮다"

  • 더해서, 비동기식으로 운영되며

  • 크로스 도메인에서 사용 가능하다.


마지막, 느슨한 결합을 위해선?

스프링 DI, IoC 를 처음 배울 때 처음 알았던 "느슨한 결합", 어떤 목적을 가져야 하는것일까?

느슨한 결합의 장점!

  1. 새로운 기능을 개발하거나, 기존 기능을 수정하고 확장하는 게 쉽다 ( 결합이 느슨하므로, 결합된 코드가 많이 수정되거나 완전히 바뀔 일이 없다 )

  2. 유지 보수가 쉽다 ( 위와 같은 이유)

느슨한 결합의 원칙!

원칙 : 두 객체가 상호작용 하지만, 서로에 대해서 잘 몰라야 한다


0개의 댓글