옵저버 패턴이란

  • 한 객체의 상태 변화에 따라 다른 객체의 상태도 연동되도록 하고 자동으로 내용이 갱신되는 방식으로 일대다 객체 의존 관계를 구성하는 패턴이다.
  • 데이터의 변경이 발생했을 경우 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보하고자 할 때 유용하다.
    • ex) 새로운 파일이 추가되거나 기존 파일이 삭제되었을 때 탐색기는 다른 탐색기에게 즉시 변경을 통보해야 한다.
    • ex) 차량 연료량 클래스는 연료량이 부족한 경우 연료량에 관심을 가지는 구체적인 클래스에 직접 의존하지 않는 방식으로 연료량 변화를 통보해야 한다.
    • ‘행위 패턴’ 중 하나
  • 옵저버 패턴은 통보 대상 객체의 관리를 Subject 클래스와 Observer 인터페이스로 일반화한다.
    • 이를 통해 데이터 변경을 통보하는 클래스는 통보 대상 클래스나 객체에 대한 의존성을 없앨 수 있다.
    • 결과적으로 통보 대상 클래스나 대상 객체의 변경에도 통보하는 클래스(ConcreteSubject)를 수정없이 그대로 사용할 수 있다.
    • 대부분 상태를 저장하고 있는 주제 인터페이스를 구현한 하나의 주제 객체와 주제 객체에 의존하고 있는 옵저버 인터페이스를 구현한 여러 개의 옵저버 객체가 있는 디자인을 바탕으로 한다. 결국, 위와 같은 의미이다.

역할이 수행하는 작업

  • Observer
    • 데이터의 변경을 통보 받는 인터페이스
    • 즉, Subject에서는 Observer 인터페이스의 update 메소드를 호출함으로써 ConcreteSubject의 데이터 변경을 ConcreteObserver에게 통보한다.
  • Subject
    • ConcreteObserver 객체를 관리하는 요소
    • Observer 인터페이스를 참조해서 ConcreteObserver를 관리하므로 ConcreteObserver의 변화에 독립적일 수 있다.
  • ConcreteSubject
    • 변경 관리 대상이 되는 데이터가 있는 클래스(통보하는 클래스)
    • 데이터 변경을 위한 setState 메소드가 있다.
    • setState 메소드에서는 자신의 데이터인 subjectState를 변경하고 Subject의 nofityObservers 메소드를 호출해서 ConcreteObserver 객체에 변경을 통보한다.
  • ConcreteObserver
    • ConcreteSubject의 변경을 통보 받는 클래스
    • Observer 인터페이스의 update 메소드를 구현함으로써 변경을 통보받는다.
    • 변경된 데이터는 ConcreteSubject의 getState 메소드를 호출함으로써 변경을 조회한다.

예시

- 여러 가지 방식으로 성적을 출력해보기. - 입력된 성적 값을 출력하는 프로그램

ScoreRecord 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 입력된 점수를 저장하는 클래스
*/
public class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
private DataSheetView dataSheetView;

public void setDataSheetView(DataSheetView dataSheetView) {
this.dataSheetView = dataSheetView;
}

// 새로운 점수를 추가하면 출력하는 것에 변화를 통보(update())하여 출력하는 부분 갱신.
public void addScore(int score) {
scores.add(score); // scores 목록에 주어진 점수를 추가한다.
dataSheetView.update();
}

// 출력하는 부분에서 변화된 내용을 얻어감.
public List<Integer> getScoreRecord() {
return scores;
}
}

DataSheetView 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 1. 출력 형태 : 목록 형태로 출력하는 클래스
*/
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); // 조회된 점수를 viewCount 만큼만 출력한다.
}

// 점수를 출력한다.
private void displayScores(List<Integer> record, int viewCount) {
System.out.println("List of " + viewCount + " entries: ");
for (int i = 0; i < viewCount && i < record.size(); i++) {
System.out.println(record.get(i) + " ");
}
System.out.println();
}
}

ObserverClient 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ObserverClient {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
// 3개까지의 점수만 출력.
DataSheetView dataSheetView = new DataSheetView(scoreRecord, 3);
scoreRecord.setDataSheetView(dataSheetView);

for (int i = 1; i <= 5; i++) {
int score = i * 10;
System.out.println("Adding : " + score);
// 10, 20, 30, 40, 50을 추가한다.
// 추가할 때마다 최대 3개의 점수만 출력한다.
scoreRecord.addScore(score);
}
}
}
  • ScoreRecord 클래스의 addScore() 메소드가 호출되면 ScoreRecord 클래스는 자신의 필드인 scores 객체에 점수를 추가한다.
    • 그리고 DataSheetView 클래스의 update() 메소드를 호출함으로써 성적을 출력하도록 요청한다.
  • DataSheetView 클래스는 ScoreRecord 클래스의 getScoreRecord() 메소드를 호출해 출력할 점수를 구한다.
    • 이때 DataSheetView 클래스의 update() 메소드에서는 구한 점수 중에서 명시된 개수만큼(viewCount)의 점수만 출력한다.

문제점

  1. 성적을 다른 형태로 출력하는 경우
  • 성적을 목록으로 출력하지 않고 성적의 최소, 최대 값만 출력하려면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
private MinMaxView minMaxView;

public void setMinMaxView(MinMaxView minMaxView) {
this.minMaxView = minMaxView;
}

public void addScore(int score) {
scores.add(score);
minMaxView.update();
}

public List<Integer> getScoreRecord() {
return scores;
}
}

점수 변경에 대한 통보 대상 클래스가 다른 대상 클래스(DataSheetView -> MinMaxView)로 바뀌면 기존 코드(ScoredRecord 클래스)의 내용을 수정해야 하므로 OCP에 위배된다.

  1. 동시 혹은 순차적으로 성적을 출력하는 경우
  • 성적이 입력되었을 때 최대 3개 목록, 최대 5개 목록, 최소/최대 값을 동시에 출력하려면?
  • 처음에는 목록으로 출력하고 나중에는 최소/최대 값을 출력하려면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ScoreRecord {
private List<Integer> scores = new ArrayList<>();
private MinMaxView minMaxView;
private DataSheetView dataSheetView;

public void setMinMaxView(MinMaxView minMaxView) {
this.minMaxView = minMaxView;
}

public void setDataSheetView(DataSheetView dataSheetView){
this.dataSheetView = dataSheetView;
}

// 새로운 점수를 추가하면 출력하는 것에 변화를 통보(update())하여 출력하는 부분을 갱신한다.
public void addScore(int score) {
scores.add(score);
dataSheetView.update(); // scores가 변경됨을 통보한다.
minMaxView.update(); // scores가 변경됨을 통보한다.
}

public List<Integer> getScoreRecord() {
return scores;
}
}

이 경우에도 점수 변경에 대한 통보 대상 클래스가 다른 클래스로 바뀌면(DataSheetView -> MinMaxView) 기존 코드의 내용을 수정해야 하므로 OCP에 위배된다.

즉, 성적 변경을 새로운 클래스에 통보할 때마다 ScoreRecord 클래스의 코드를 수정해야 하므로 번거롭고 재사용하기가 어렵다.

위의 그림처럼 양방향의 의존 관계를 가지고 있기 때문에 통보 대상 클래스가 변경되면 ScoreRecord 클래스도 수정해야 한다. 따라서 우리는 이 문제를 해결해야 한다.

해결책

문제를 해결하기 위해서는 공통 기능을 상위 클래스 및 인터페이스로 일반화하고 이를 활용하여 통보하는 클래스를 구현해야 한다.

  • 즉, ScoreRecord 클래스에서 변화되는 부분을 식별하고 이를 일반화시켜야 한다.
  • 이를 통해 성적 통보 대상이 변경되더라도 ScoreRecord 클래스를 그대로 재사용할 수 있다.
  • ScoreRecord 클래스에서 하는 작업
    • 통보 대상인 객체를 참조하는 것을 관리(추가/제거) -> Subject 클래스로 일반화
    • addScore 메소드 : 각 통보 대상인 객체의 update() 메소드를 호출 -> Observer 인터페이스로 일반화
  1. ScoreRecord 클래스의 addScore(상태 변경) 메소드 호출
    • 자신의 성적 값을 저장한다.
    • 상태가 변경될 때마다 Subject 클래스의 notifyObservers() 메소드를 호출한다.
  2. Subject 클래스의 notifyObservers() 메소드 호출
    • Observer 인터페이스를 통해 성적 변경을 통보한다.
    • DataSheetView 클래스의 update() 메소드 호출
    • MinMaxView 클래스의 update() 메소드 호출

개선 시킨 후의 코드

Observer 인터페이스

1
2
3
4
public interface Observer {
// 데이터 변경을 통보했을 때 처리하는 메소드
void update();
}

Subject 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 추상화된 변경 관심 데이터
* 즉, 데이터에 공통적으로 들어가야 하는 메소드들 -> 일반화
*/
public abstract class Subject {

// 추상화된 통보 대상 목록.
// 즉, 출력 형태에 대한 Observer
private List<Observer> observers = new ArrayList<Observer>();

// 통보 대상 추가.
public void attach(Observer observer) {
observers.add(observer);
}

// 통보 대상 제거.
public void detach(Observer observer) {
observers.remove(observer);
}

// 각 통보 대상에게 변경을 통보한다.
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}

ScoreRecord 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 구체적인 감시 대상 데이터
* 출력 형태 2개를 가질 때
*/
public class ScoreRecord extends Subject {
private List<Integer> scores = new ArrayList<>();

// 새로운 점수를 추가. -> 상태 변경
public void addScore(int score) {
scores.add(score); // scores 목록에 주어진 점수를 추가한다.
notifyObservers(); // scores 가 변경됨을 각 통보 대상에게 통보한다.
}

public List<Integer> getScoreRecord() {
return scores;
}
}

DataSheetView 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 1. 출력 형태 : 목록 형태로 출력하는 클래스
*/
public class DataSheetView implements Observer {
private ScoreRecord scoreRecord;
private int viewCount;

public DataSheetView(ScoreRecord scoreRecord, int viewCount) {
this.scoreRecord = scoreRecord;
this.viewCount = viewCount;
}

// 점수를 출력한다.
private void displayScores(List<Integer> record, int viewCount) {
System.out.println("List of " + viewCount + " entries: ");
for (int i = 0; i < viewCount && i < record.size(); i++) {
System.out.println(record.get(i) + " ");
}
//System.out.println();
}

// 점수의 변경을 통보 받는다.
@Override
public void update() {
List<Integer> record = scoreRecord.getScoreRecord(); // 점수를 조회한다.
displayScores(record, viewCount); // 조회된 점수를 viewCount 만큼만 출력한다.
}
}

MinMaxView 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 2. 출력 형태 : 최대, 최소 값만을 출력한다.
*/
public class MinMaxView implements Observer {
private ScoreRecord scoreRecord;

public MinMaxView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}

private void displayScores(List<Integer> record) {
int min = Collections.min(record);
int max = Collections.max(record);
System.out.println("Min : " + min + ", Max : " + max);
}

@Override
public void update() {
List<Integer> record = scoreRecord.getScoreRecord();
displayScores(record);
}

}

ObserverClient 클래스에서의 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ObserverClient {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();

// 3개까지만 데이터를 출력한다.
DataSheetView dataSheetView = new DataSheetView(scoreRecord, 3);
// 최대, 최소 값만 출력한다.
MinMaxView minMaxView = new MinMaxView(scoreRecord);

// 각 통보 대상 클래스를 Observer 로 추가한다.
scoreRecord.attach(dataSheetView);
scoreRecord.attach(minMaxView);

for (int i = 1; i <= 5; i++) {
int score = i * 10;
System.out.println();
System.out.println("Adding : " + score);
scoreRecord.addScore(score);
}
}
}
  • Observer : 추상화된 통보 대상
  • DataSheetView, MinMaxView : Observer를 구현함으로써 구체적인 통보 대상이 된다.
  • Subject : 성적 변경에 관심이 있는 대상 객체들을 관리한다.
  • ScoreRecord : Subject를 상속받음으로써 구체적인 통보 대상을 직접 참조하지 않아도 된다. notifyObservers()를 호출함으로써 Subject 클래스에서 각 통보 대상들에게 변경 사항을 통보한다.
  • 이렇게 Observer 패턴을 이용하면 ScoreRecord 클래스의 코드를 변경하지 않고도 새로운 관심 클래스 및 객체를 추가/제거하는 것이 가능해진다.

느낀 점

결국, 처음에는 양방향으로 참조를 해서 강한 의존성을 갖고 있었다. 하지만, Observer 패턴을 적용함으로써 의존성을 약하게 결합하도록 하고 양방향 참조를 하지 않도록 함으로써 디자인 원칙을 위배하지 않으면서 데이터 변경을 적절하게 통보할 수 있게 되었다.

이처럼 느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난하게 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다. (객체 사이의 상호 의존성을 최소화 할 수 있기 때문이다.)

참고