CS

디자인 패턴 - 옵저버 패턴

ri5 2023. 2. 1. 11:03

옵저버란?

Observer를 직역하자면 어떤 일을 하는지 지켜보지만 어떠한 행동을 취하지 않는 사람이라는 뜻이다. 옛날에 우리가 많이 했었던 스타크래프트 있던 옵저버라는 유닛 또한 시야를 밝혀주고 아군들에게 공유할 뿐 어떠한 공격도 할 수 없는 유닛이였다. 그렇다면 프로그래밍에서의 옵저버는 무슨 역활을 하고 의미하는 것일까?

 

뉴스 서비스를 통해 이해하기

처음에는 A사, B사, C사의 외부 API를 받아서 보여주는 기능을 개발하였습니다. 아래와 같은 시스템을 구현해야된다고 했을 때에 옵저버 패턴 형식으로 구현하게 된다면 옵저버 패턴이 어떠한 장점을 가지고 있고 어떠한 상황에 사용해야 되는지 쉽게 알 수 있다.

뉴스 구독 서비스의 구현

뉴스 에이전시

public class NewsAgency {
    private String news;
    private List<Channel> channels = new ArrayList<>();

    public void addObserver(Channel channel) {
        this.channels.add(channel);
    }

    public void removeObserver(Channel channel) {
        this.channels.remove(channel);
    }

    public void setNews(String news) {
        this.news = news;
        for (Channel channel : this.channels) {
            channel.update(this.news);
        }
    }
}

뉴스 채널

public class NewsChannel implements Channel {
    private String news;

    @Override
    public void update(Object news) {
        this.setNews((String) news);
    } 

    // standard getter and setter
}

public interface Channel {
    public void update(Object o);
}

테스트

NewsAgency observable = new NewsAgency();
NewsChannel observer = new NewsChannel();

observable.addObserver(observer);
observable.setNews("news");
assertEquals(observer.getNews(), "news");

사용자가 새롭게 구독한다고 한다면 NewsChannel 인스턴스를 observers 목록에 추가 하고 NewsAgency 상태를 변경하면 NewsChannel 인스턴스가 업데이트됩니다. 옵저버 패턴의 기본적인 원리는 주체(NewsAgency)가 안에 있는 옵저버들을 관리하고 주체의 내용이 업데이트 되면 옵저버들에게 내용을 전달하여 갱신되는 구조를 볼 수 있다.

 

그림으로 표현한 옵저버 패턴의 동작원리

옵저버 패턴은 그림과 같이 하나의 주체를 통해 의존하고 있는 다른 객체들에게 전달할 수 있는 상황에 적절하게 사용되는 것을 알 수 있었다. 하지만 옵저버 패턴이 만들어진 배경이 무엇이고 어떠한 문제를 풀기위해 만들어 진 것인지 아직까지는 모른다. 배경과 어떠한 문제해결을 하는지 모르고 사용할 수는 있지만 적절한 상황에 적용하기는 쉽지 않을 것이다.

 

 

옵저버 패턴을 사용하지 않고 구현한 구독 서비스

뉴스 플랫폼

public class ANewsPlatform {
	
    private List<News> newsList; 
    
    public News getLatestNews() {
    	...
    }
    
    public void addNews() {
    	...
    }
}

구독자 클래스

public Subscriber {
    private News latestNews;
    private String email;
    
    public void updateLatestNews(News news) {
    	this.updateLatestNews = news
    }
}

 

 

 

사용

public void sendNotification() {

    ANewsPlatform newsFlatform = getANewsPlatform();
    
    List<Subscriber> subscribers = findSubscribers();
    
    subscribers
    	.foreach(subscriber -> subscriber.updateLatestNews(newsFlatform.getLatestNews()))
}

디자인 패턴은 성능에 유리하게 하기 위한 점도 있지만 근본적으로는 변경에 대해 유연하고 확장에 편리하게 하기 위해 만들어진 패턴이다.  위와같이 개발한다고 했었을 때 PremiumSubscriber(프리미엄 구독자)가 새로 추가되거나 A뉴스사가 서비스를 폐지하거나 다른 뉴스사로 변경하게 된다면 어떻게 될까? 해당 클래스를 의존하여 사용하고 있는 모든 코드가 변경되는 문제가 발생하게 됩니다.

 

그래서 옵저버 패턴을 활용하여 추상화된 인터페이스를 통해 느슨한 결합을 가져가게 되는 것입니다. NewsChannel이 아니라 PremiumNewsChannel로 변경되거나 새로운 뉴스 에이전시가 추가되어도 상관없이 쉽게 구현할 수 있다는 것이 장점입니다.

 

자바에서의 옵저버 패턴 사용방법

옵저버 인터페이스를 구현하여 사용

자바8 이하에서는 유틸클래스 Observer 인터페이스를 제공하여 쉽게 옵저버 패턴을 구현할 수 있도록 제공하고 있다. 

 

뉴스채널

public class ONewsChannel implements Observer {

    private String news;

    @Override
    public void update(Observable o, Object news) {
        this.setNews((String) news);
    }

    // standard getter and setter
}

뉴스에이전시

public class ONewsAgency extends Observable {
    private String news;

    public void setNews(String news) {
        this.news = news;
        setChanged();
        notifyObservers(news);
    }
}

테스트

ONewsAgency observable = new ONewsAgency();
ONewsChannel observer = new ONewsChannel();

observable.addObserver(observer);
observable.setNews("news");
assertEquals(observer.getNews(), "news");

여기에서 두 번째 파라미터는에서 볼 수 있듯이 Observable 에서 가져옵니다. observable 을 정의하려면 Java의 Observable 클래스를 상속받아서 사용해야 합니다. 옵저버의 update() 메서드를 직접 호출할 필요가 없다 . setChanged()  notifyObservers() 만 호출 하면 Observable 클래스가 나머지 작업을 수행하기 때문이다.

 

자바9 부터 해당 유틸 클래스는 사용되지 않기 시작했다. 그이유는 Observable이 추상화된 인터페이스가 아닌 구현 클래스에 의존한다는 점과 개발자는 Observable 의 동기화된 메서드 중 일부를 재정의 하고 스레드 안전성을 방해할 수 있기 때문이다. 

 

PropertyChangeListener 로 구현

PropertyChangeSupport인스턴스에 대한 참조를 유지해야 합니다. 클래스의 속성이 변경될 때 옵저버에 전달할 때 사용되기 때문이다. 

 

뉴스 에이전시 클래스

public class PCLNewsAgency {
    private String news;

    private PropertyChangeSupport support;

    public PCLNewsAgency() {
        support = new PropertyChangeSupport(this);
    }

    public void addPropertyChangeListener(PropertyChangeListener pcl) {
        support.addPropertyChangeListener(pcl);
    }

    public void removePropertyChangeListener(PropertyChangeListener pcl) {
        support.removePropertyChangeListener(pcl);
    }

    public void setNews(String value) {
        support.firePropertyChange("news", this.news, value);
        this.news = value;
    }
}

support.firePropertyChange()를 통해 옵저버의 정보를 갱신하는데 여기서 첫 번째 파라미터("news")는 관찰된 속성의 이름이다. 그뒤로 따라오는 두 번째(this.news)세 번째 파라미터(value)는 이전 값과 새 값이다.

 

뉴스 채널

public class PCLNewsChannel implements PropertyChangeListener {

    private String news;

    public void propertyChange(PropertyChangeEvent evt) {
        this.setNews((String) evt.getNewValue());
    }

    // standard getter and setter
}

여기서 옵저버에 정보를 전달해주는 주체는 PropertyChangeListener를 구현하여야 한다.

 

테스트

PCLNewsAgency observable = new PCLNewsAgency();
PCLNewsChannel observer = new PCLNewsChannel();

observable.addPropertyChangeListener(observer);
observable.setNews("news");

assertEquals(observer.getNews(), "news");