옵서버(Observer)는 말 그대로 무언가를 감시하고 관측하는 역할을 한다. 디자인 패턴에선 어떠한 이벤트를 감지하는 패턴을 의미한다.
성적을 저장하는 ScoreRecord 클래스와 점수를 출력하는 DataSheetView 클래스가 있다고 가정하자. ScoreRecord 클래스의 addScore 메서드가 실행될 때 성적을 출력하려면 ScoreRecord 클래스는 DataSheetView 객체를 참조해야 한다.
ScoreRecord 클래스의 scores 객체에 점수를 추가하고, DataSheetView 클래스의 update 메서드를 호출해 출력할 점수를 구한다.
public class ScoreRecord{
private List<Integer> scores=new ArrayList<Integer>();//점수 저장
private DataSheetView dataSheetView;//목록 형태로 점수를 출력하는 클래스
public void setDataSheetView(DataSheetView dataSheetView){
this.dataSheetView=dataSheetView;
}
public void addScore(int score){//새로운 점수 추가
scores.add(score);
dataSheetView.update();
}
public List<Integer> getScoreRecord(){
return scores;
}
}
public class DataSheetView{
private ScoreRecord scoreRecord;
private int viewCount;
public DataSheetView(ScoreRecord scoreRecord, int viewCount){
this.scoreRecord=scoreRecord;
this.viewCount=viewCount;
}
public void update(){
List<Integer> record=scoreRecord.getScoreRecord();
displayScores(record,viewCount);
}
public void displayScores(List<Integer> record,int viewCount){
...
}
}
public class Client{
public static void main(String[] args){
ScoreRecord scoreRecord = new ScoreRecord();
DataSheetView dataSheetView = new DataSheetView(scoreRecord,3);
scoreRecord.setDataSheetView(dataSheetView);
...
}
}
DataSheetView 클래스의 update 메서드가 실행될 때는 ScoreRecord 클래스에서 점수를 조회하는 getScoreRecord 메서드를 호출하려고 ScoreRecord 객체를 DataSheetView 클래스의 생성자에 전달했다.
마찬가지로 ScoreRecord 클래스는 점수가 추가될 때, 즉 addScore 메서드가 호출될 때 DataSheetView의 update 메서드를 호출할 필요가 있다. 이를 위해 ScoreRecord 클래스의 setDataSheetView 메서드를 호출해 DataSheetView 객체를 전달한다.
기존의 DataSheetView 클래스 대신 최소/최대 값을 출력하는 MinMaxView 클래스를 추가해야 한다. 그리고 ScoreRecord 클래스는 DataSheetView 클래스가 아니라 MinMaxView 클래스에 성적 변경을 통보해야 한다.
public class ScoreRecord{
private List<Integer> scores=new ArrayList<Integer>();//점수 저장
private MinMaxView minMaxView;
public void setMinMaxView(MinMaxView minMaxView){//MinMaxView를 설정
this.minMaxView=minMaxView;
}
public void addScore(int score){//새로운 점수 추가
scores.add(score);
minMaxView.update();//MinMaxView에게 점수 변경을 통보
}
public List<Integer> getScoreRecord(){
return scores;
}
}
기존의 ScoreRecord 클래스의 addScore 메서드를 직접적으로 변경해서 MinMaxView 클래스의 update 메서드를 호출했다. 그런데 이는 OCP를 위배한다. 점수가 입력되었을 때 지정된 특정 대상(DataSheetView)에게 고정적으로 통보하도록 했는데, 다른 대상 클래스(MinMaxView)에게 점수가 입력되었음을 통보하려면 ScoreRecord 클래스의 변경이 불가피하기 때문이다.
목록으로 출력하는 것은 DataSheetView를 활용하고 최소/최대 값을 출력하는 것은 MinMaxView를 활용해야 한다. 따라서 ScoreRecord 클래스는 다시 변경되어야 한다.
public class ScoreRecord{
private List<Integer> scores=new ArrayList<Integer>();//점수 저장
//복수 개의 DataSheetView
private List<DataSheetView> dataSheetView = new List<DataSheetView>();
private MinMaxView minMaxView;
public void setDataSheetView(DataSheetView dataSheetView){//DataSheetView 추가
this.dataSheetView=dataSheetView;
}
public void setMinMaxView(MinMaxView minMaxView){//MinMaxView 설정
this.minMaxView=minMaxView;
}
public void addScore(int score){//새로운 점수 추가
scores.add(score);
for(DataSheetView dataSheetView:dataSheetViews)
dataSheetView.update(); //각 DataSheetView에 값의 변경을 통보
minMaxView.update(); //MinMaxView에게 점수 변경을 통보
}
public List<Integer> getScoreRecord(){
return scores;
}
}
이 경우에도 성적의 통보 대상이 변경된 것을 반영하기 위해 ScoreRecord 클래스의 코드를 수정하게 되었다.
성적 통보 대상이 변경되어도 ScoreRecord 클래스를 그대로 재사용할 수 있어야 한다. 따라서 ScoreRecord 클래스에서 변화되는 부분을 식별하고 이를 일반화해야 한다.
ScoreRecord 클래스의 통보 대상인 객체 참조 관리와, addScore 메서드의 각 통보 대상 객체의 update 메서드 호출 기능은 동일하게 발생한다. 이러한 공통 기능을 상위 클래스 및 인터페이스로 일반화하고 이를 활용해 ScoreRecord를 구현하는 방식으로 설계를 변경하는 것이 좋다.성적 변경에 관심이 있는 대상 객체를 관리하는 Subject 클래스가 정의되었다. attach와 detach 메서드로 성적 변경에 관심이 있는 대상 객체를 추가하거나 제거한다. 이때 성적 변경 통보 수신 측면에서 DataSheetView 클래스와 MinMaxView 클래스는 동일하므로 Subject 클래스는 Observer 인터페이스를 구현하여 성적 변경에 관심이 있음을 보여준다.
ScoreRecord 클래스의 addScore 메서드가 호출되면 자신의 성적 값을 저장 후 Subject 클래스의 notifyObserver 메서드를 호출해서 DataSheetView과 MinMaxView 객체의 update 메서드를 호출한다.
public interface Observer{//추상화 된 통보 대상
public abstract void update()//데이터의 변경을 통보했을 때 처리 메서드
}
public abstract class Subject{//추상화된 변경 관심 대상 데이터
private List<Observer> observers = new ArrayList<Observer>();//추상화된 통보 대상 목록
public void attach(Observer observer){
observers.add(observer);
}
public void detach(Observer observer){
observers.remove(observer);
}
//통보 대상 목록, 즉 observers의 각 옵서버에게 변경을 통보
public void notifyObserver(){
for(Observer o:observers){
o.update();
}
}
}
public class ScoreRecord extends Subject{
private List<Integer> scores = new ArrayList<Integer>();
public void addScore(int socore){
scores.add(score);
//데이터가 변경되면 Subject 클래스의 notifyObservers 메서드를 호출하여
//각 옵서버(통보 대상 클래스)에게 데이터의 변경을 통보함
notifyObservers();
}
public List<Integer> getScoreRecord(){
return scores;
}
}
public class DataSheetView implements Observer{
...
}
public class MinMaxView implements Observer{
...
}
public class Client{
...
scoreRecord.attach(dataSheetView3);
scoreRecord.attach(dataSheetView5);
socreRecord.attach(minMaxView);
...
}
성적 변경에 관심이 있는 대상 객체들의 관리는 Subject 클래스에서 구현하고 ScoreRecord에서 이를 상속받아서 이제 DataSheetView, MinMaxView를 직접 참조할 필요가 없어졌다. 그러므로 ScoreRecord의 변경 없이 새로운 관심 클래스 및 객체를 추가, 제거하는 것이 가능하다.
관심 객체의 관리는 Subject 클래스에서 수행하므로 ScoreRecord는 변경하지 않고 결과를 출력할 수 있다.
옵서버 패턴은 데이터의 변경이 발생했을 때 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보하고자 할 때 유용하다. 예로 파일의 탐색기를 여러개 실행하는 상황에서 하나의 탐색기에서 파일 시스템을 변경했을 때 즉시 다른 탐색기에게 변경 사항을 통보해야 한다.
옵서버 패턴은 통보 대상의 객체 관리를 Subject 클래스와 Observer 인터페이스로 일반화한다. 데이터 변경을 통보하는 클래스(ConcreteSubject)는 통보 대상 클래스나 객체(ConcreteObserver)에 대한 의존성을 없앨 수 있다. 결과적으로 옵서버 패턴은 통보 대상 클래스나 대상 객체의 변경에도 ConcreteSubject 클래스를 수정 없이 그대로 사용할 수 있도록 한다.
데이터의 변경을 통보 받는 인터페이스.
Subject에서는 Observer 인터페이스의 update를 호출함으로써 ConcreteSubject의 데이터 변경을 ConcreteObserver에게 통보한다.
ConcreteObserver 객체를 관리하는 요소.
Observer 인터페이스를 참조해서 ConcreteObjserver를 관리하므로 ConcreteObserver 변화에 독립적일 수 있따.
변경 관리 대상이 되는 데이터가 있는 클래스.
데이터 변경을 위한 메서드 setState가 있고, setState에서는 자신만의 데이터인 subjectState를 변경하고 Subject의 notifyObservser를 호출해서 ConcreteObserver 객체에 변경을 통보한다.
ConcreteSubject의 변경을 통보받는 클래스.
Observer 인터페이스의 update 메서드를 구현함으로써 변경을 통보받는다. 변경된 데이터는 ConcreteSubject의 getState 메서드를 호출하여 변경을 조회한다.