본문 바로가기

Design Patterns

Observer Pattern

옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 이를 알리고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다. 이때 상태가 바뀌는 주체를 주제(Subject), 주제의 상태 변경에 따라 갱신되는 주제를 관찰, 구독하는 객체를 옵저버(Observer)라고 한다. 옵저버 패턴은 주제의 상태변경을 알아야하는 객체들이 주제의 상태변경시 자동으로 상태가 변경이 되거나 특정한 행동을 하도록 하면서도 주제와 옵저버들간 느슨한 연관관계를 가짐으로서 유연한 설계가 가능하다는 장점이 있다.

 

좀더 구체적으로 특징을 살펴보면,

첫 번째로 옵저버 패턴은 주제에 대한 이벤트 발생을 옵저버가 구독하는 형태를 취하며, 주제는 구체적인 옵저버에 대한 정보를 알 수 없고, 옵저버 또한 주제에 대한 정보를 갖고 있지 않기 때문에 관계가 느슨해진다는 특징이 있다.

두 번째로 옵저버의 추가 및 제거가 간단하다는 특징이 있다. 이는 주제에서 옵저버 인터페이스를 구현한 구현체의 목록을 관리하고 있고, 이 목록은 주제 객체에서 작동하는 로직에 어떠한 영향도 끼치지 않기 때문에 옵저버를 추가하거나 제거하는 것의 영향도가 적다.

세 번째로 새로운 옵저버를 구현하는 것도 간단하다. 새로운 구현체를 생성하였더라도, 옵저버 인터페이스를 구현하였다면 다른 옵저버들과 마찬가지로 주제가 관리하는 리스트에만 추가하면 그만이다.

네 번째로 주제와 옵저버는 독립적으로 사용이 가능하다. 주제나 옵저버는 서로 느슨한 연결관계를 갖고 있기 때문에 그만큼 의존성이 적어 독립적으로 재사용이 가능하다.

마지막으로 주제나 옵저버가 바뀌더라도 서로에게 영향일 미치지 않는다. 구현체의 내용이 바뀌더라도 인터페이스에서 정의된 내용에 따라 구현되었다면, 주제 구현제의 변경이 옵저버 구현체의 작동에 영향을 미치지 않으며, 옵저버 구현체의 변경 또한 주제 구현체의 작동에 영향을 미치지 않는다.

 

주제와 옵저버간 관계를 도식화하면 다음과 같다.

 

옵저버 패턴을 구현한 코드는 아래와 같다.

 

Subject.java

import java.util.ArrayList;
import java.util.List;

public interface Subject {
	final List<Observer> observers = new ArrayList<>();
			
	default void addObserver(Observer o) {
		observers.add(o);
	}
	
	default void deleteObserver(Observer o) {
		final int idx = observers.indexOf(o);
		if (idx > -1) {
			observers.remove(idx);
		}
	}
	
	void notifyObservers();
}

 

주제 인터페이스는 옵저버 리스트인 observers를 갖고 있으며,  addObserver를 사용하여 옵저버를 추가할 수 있고, deleteObserver를 사용하여 옵저버를 제거할 수 있다. notifyObservers는 각 옵저버들에게 주제의 변경을 전파하는 역할을 하며, 이는 구현체에서 구현하였다.

 

Observer.java

public interface Observer {
	void update(Integer price);
}

옵저버 인터페이스는 주제의 변경시 옵저버의 내용을 변경하거나 로직을 실행시키기 위한 update 메소드를 갖는다.

 

Goods.java

public class Goods implements Subject {
	private Integer price;
	
	public Integer getPrice() {
		return price;
	}
	
	public void setPrice(Integer price) {
		this.price = price;
		notifyObservers();
	}
	
	@Override
	public void notifyObservers() {
		observers.forEach(observer -> observer.update(price));
	}
}

예시로 들은 Goods는 주제 인터페이스의 구현체이며, 상태로 필드값 price를 갖는다. 이는 상품의 상태를 가격으로 추상화한 것으로 가격의 변경이 상태의 변경을 의미하는 것이다. 그렇기 때문에 setPrice를 통하여 상태가 변하는 경우 이 변경을 notifyObservers를 이용하여 옵저버에 전달해주며, notifyObservers에서는 주제가 갖고 있는 옵저버의 목록을 순환하면서 update를 호출하여 각 옵저버에서 정의하고 있는 내용에 따라 작동하도록 하고 있다.

 

옵저버의 구현체로는 DisplayForCustomer와 Dashboard가 있는데 이는 각각 주제인 Goods의 변경에 따라 자신의 상태를 변경하거나 어떠한 행동을 취할지를 구현한 것이다.

 

DisplayForCustomer.java

public class DisplayForCustomer implements Observer {
	private Integer price;

	@Override
	public void update(Integer price) {
		this.price = price;
		display();
	}

	private void display() {
		System.out.printf("Diplayed Price: %d\n", price);
	}
}

 

Dashboard.java

public class Dashboard implements Observer {
	private Integer price;
	
	@Override
	public void update(Integer price) {
		this.price = price;
		alert();
	}
	
	public void alert() {
		System.out.printf("Dashboard Price: %d\n", price);
	}
}

 

DisplayForCustomer에서는 자신의 가격을 변경하면서 display 메소드를 실행시켜 변경된 가격을 보여주었으며, Dashboard에서는 가격을 변경하면서 alert를 사용하여 대쉬보드 사용자들에게 가격변경을 알림으로 전달하였다. 이처럼 각 옵저버들의 구체적인 행동을 update 내에서 구현되지만, 옵저버들은 자신의 주제에 대한 어떠한 정보도 갖고 있지 않으며, 주제 또한 Observer 인터페이스에 대해서는 알지만, 구현체에 대한 구체적인 정보는 갖고 있지 않다는 것을 알 수 있다.

 

테스트 코드는 아래와 같다.

 

ObserverPatternTest.java

public class ObserverPatternTest {
	public static void main(String[] args) {
		final Dashboard dashboard = new Dashboard();
		final DisplayForCustomer displayForCustomer = new DisplayForCustomer();
		
		final Goods goods = new Goods();
		goods.addObserver(dashboard);
		goods.addObserver(displayForCustomer);
		
		goods.setPrice(10000);
		System.out.println("========================");
		goods.setPrice(15000);
	}
}

 

옵저버 패턴은 이처럼 사용자가 직접 구현할 수도 있지만, Java내에 내장된 Observable 클래스를 상속하여 주제를 생성하고, Observer 인터페이스를 구현하여 사용할 수도 있다. 옵저버가 연락을 받는 방법에서 차이가 있을 뿐 사용에는 큰 차이가 없다.

 

Goods.java

import java.util.Observable;

public class Goods extends Observable {
	private Integer price;
	
	public void setPrice(Integer price) {
		this.price = price;
		setChanged();
		notifyObservers();
	}
	
	public Integer getPrice() {
		return price;
	}
}

Goods 객체는 아까와는 다르게 Observable을 상속하여 구현하는데, Observable과 직접 구현한 Subject는 각각 클래스와 인터페이스라는 점에서 차이가 있다. 그렇기 때문에 이미 객체가 다른 객체를 상속하고 있었다면, Observable은 사용할 수 없다. Java는 다중 상속을 지원하지 않기 때문이다. 이런 경우라면 앞서 설명한 것과 같이 직접 주제 인터페이스를 생성하여 사용해야한다. 또한 Observable은 주제의 상태가 변경된 경우, setChanged를 호출하여 변경을 알려야한다는 점이 차이가 있다. notifyObservers는 직접 구현하지 않으며, notifyObservers를 호출하면 Observable을 구독하고 있는 Observer들의 update가 호출된다.

 

DisplayForCustomer.java

import java.util.Observable;
import java.util.Observer;

public class DisplayForCustomer implements Observer {
	private Observable observable;
	private Integer price;
	
	public DisplayForCustomer(Observable observable) {
		this.observable = observable;
		observable.addObserver(this);
	}
	
	@Override
	public void update(Observable observable, Object arg) {
		final Goods goods = (Goods) observable;
		price = goods.getPrice();
		display();
	}
	
	private void display() {
		System.out.printf("Diplayed Price: %d\n", price);
	}
}

DisplayForCustomer는 Observer 인터페이스를 구현하였는데, update 함수를 구현하도록 되어 있다. 또한 앞서 구현한 DisplayForCustomer와 차이는 Observable에 대한 정보를 알고 있으며, 생성자 함수에서 해당 Observable에 직접 옵저버 자신을 추가하고 있다는 점이 차이가 있다.

 

Dashboard.java

import java.util.Observable;
import java.util.Observer;

public class Dashboard implements Observer {
	private Observable observable;
	private Integer price;
	
	public Dashboard(Observable observable) {
		this.observable = observable;
		observable.addObserver(this);
	}
	
	@Override
	public void update(Observable observable, Object arg) {
		final Goods goods = (Goods) observable;
		price = goods.getPrice();
		alert();
	}
	
	private void alert() {
		System.out.printf("Dashboard Price: %d\n", price);
	}
}

 

Dashboard 또한 DisplayForCustomer와 동일한 방식으로 구현되었다.

 

마지막으로 테스트 코드는 다음과 같다.

 

ObserverTest.java

public class ObserverTest {
	public static void main(String[] args) {
		final Goods goods = new Goods();
		
		final Dashboard dashboard = new Dashboard(goods);
		final DisplayForCustomer displayForCustomer = new DisplayForCustomer(goods);
		
		goods.setPrice(10000);
		System.out.println("========================");
		goods.setPrice(15000);
	}
}

 

'Design Patterns' 카테고리의 다른 글

Builder Pattern  (0) 2020.07.05