옵저버패턴 (Observer Pattern)
옵저버패턴이란?
데이터의 변경이 발생하였을 때, 상태 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보하고자 할 때 사용한다.
<br/>
예제를 통해 공부해보자.
입력된 성적을 3개 출력하는 프로그램을 의뢰받아 제작하려고한다.
다음과 같이 클래스를 설계하였다.
<img src="https://static.podo-dev.com/blogs/images/2019/07/10/origin/OXBXGU181224235508.PNG">
-
성적 입력에 관한 ScoreRecord 클래스
-
성적 N개를 출력하는 DisplaySheetView 클래스
<br/>
ScoreRecord 클래스에 addScore() 메소드가 호출되면, 출력을 DisplaySheetView 클래스를 참조하여야한다.
<br/>
다음과 같이 구현하였고, 3개의 성적을 출력하도록 하였다.
public class ScoreRecord {
private List<Integer> scores = new LinkedList<>();
private DataSheetView dataSheetView;
public void setDataSheetView(DataSheetView dataSheetView) {
this.dataSheetView = dataSheetView;
}
public void addScore(int s) {
scores.add(s);
dataSheetView.update();
}
public List<Integer> getScores() {
return scores;
}
}
public class DataSheetView {
private ScoreRecord scoreRecord;
private int n;
public DataSheetView(ScoreRecord scoreRecord, int n) {
this.scoreRecord = scoreRecord;
this.n = n;
}
public void update() {
List<Integer> scores = scoreRecord.getScores();
displayScores(scores);
}
private void displayScores(List<Integer> scores) {
for(int i =0; i < n && i < scores.size(); i++) {
System.out.print(scores.get(i) + " ");
}
System.out.println("");
}
}
public class Main {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
DataSheetView dataSheetView = new DataSheetView(scoreRecord, 3);
scoreRecord.setDataSheetView(dataSheetView);
scoreRecord.addScore(10);
scoreRecord.addScore(20);
scoreRecord.addScore(30);
scoreRecord.addScore(40);
}
}
<br/>
출력
10
10 20
10 20 30
10 20 30
<br/>
로직은 이상없이 작동하였고, 결과도 정상적이다.
<br/>
그런데 추가적인 의뢰가 들어왔다.
"성적을 출력해주는 곳이 두 곳이 더생겼어요. 한 곳은 5개 출력해주고, 또 다른 곳은 최소값, 최대값만 출력하게 추가해주세요!"
<br/>
추가적인 의뢰를 받은 개발자는 다음과같이 클래스를 설계하였다.
<img src="https://static.podo-dev.com/blogs/images/2019/07/10/origin/Z9NRNW181224235509.PNG">
-
성적 입력에 관한 ScoreRecord 클래스
-
성적 N개를 출력하는 DisplaySheetView 클래스
-
성적 최소, 최대 값을 출력하는 MinMaxSheetView 클래스
<br/>
<br/>
설계를 바탕으로 같이 구현하였다.
public class ScoreRecord {
private List<Integer> scores = new LinkedList<>();
private DataSheetView dataSheetView3;
private DataSheetView dataSheetView5;
private MinMaxSheetView minMaxSheetView;
public void setDataSheetView3(DataSheetView dataSheetView3) {
this.dataSheetView3 = dataSheetView3;
}
public void setDataSheetView5(DataSheetView dataSheetView5) {
this.dataSheetView5 = dataSheetView5;
}
public void setMinMaxSheetView(MinMaxSheetView minMaxSheetView) {
this.minMaxSheetView = minMaxSheetView;
}
public void addScore(int s) {
scores.add(s);
minMaxSheetView.update();
dataSheetView3.update();
dataSheetView5.update();
}
public List<Integer> getScores() {
return scores;
}
}
public class DataSheetView {
private ScoreRecord scoreRecord;
private int n;
public DataSheetView(ScoreRecord scoreRecord, int n) {
this.scoreRecord = scoreRecord;
this.n = n;
}
public void update() {
List<Integer> scores = scoreRecord.getScores();
displayScores(scores);
}
private void displayScores(List<Integer> scores) {
System.out.print( n + "개 출력 SHEET ");
for(int i =0; i < n && i < scores.size(); i++) {
System.out.print(scores.get(i) + " ");
}
System.out.println("");
}
}
public class MinMaxSheetView {
private ScoreRecord scoreRecord;
public MinMaxSheetView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}
public void update() {
List<Integer> scores = scoreRecord.getScores();
displayScores(scores);
}
private void displayScores(List<Integer> scores) {
System.out.print("최소값, 최대값 출력 SHEET ");
System.out.print(Collections.min(scores) + " ");
System.out.print(Collections.max(scores) + " ");
System.out.println("");
}
}
public class Main {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
scoreRecord.setDataSheetView3(new DataSheetView(scoreRecord, 3));
scoreRecord.setDataSheetView5(new DataSheetView(scoreRecord, 5));
scoreRecord.setMinMaxSheetView(new MinMaxSheetView(scoreRecord));
scoreRecord.addScore(10);
scoreRecord.addScore(20);
scoreRecord.addScore(30);
scoreRecord.addScore(40);
scoreRecord.addScore(50);
}
}
<br/>
출력
최소값, 최대값 출력 SHEET 10 10
3개 출력 SHEET 10
5개 출력 SHEET 10
최소값, 최대값 출력 SHEET 10 20
3개 출력 SHEET 10 20
5개 출력 SHEET 10 20
최소값, 최대값 출력 SHEET 10 30
3개 출력 SHEET 10 20 30
5개 출력 SHEET 10 20 30
최소값, 최대값 출력 SHEET 10 40
3개 출력 SHEET 10 20 30
5개 출력 SHEET 10 20 30 40
최소값, 최대값 출력 SHEET 10 50
3개 출력 SHEET 10 20 30
5개 출력 SHEET 10 20 30 40 50
<br/>
ScoreRecord 클래스에 참조값을 더 하는 것으로 해결하였다. 코드가 길어졌다.
로직이 정상적으로 작동하였고, 출력결과도 정확하였다.
<br/>
그런데 추가적인 의뢰가 들어왔다.
"성적을 출력해주는 곳이 N 곳이 더생겼어요. 최소값만 출력하는 곳, 최대값만 출력하는 곳, 평균값만 출력하는 곳 등 N개의 유형으로 출력하고 싶어요!"
<br/>
2개를 추가하는 것은 참조값을 2개 늘려 구현하였으므로, 크게 문제가 없었다.
근데 이제 N개를 추가해야 하는 의뢰에, 참조값을 N개 추가 할 수는 없는 노릇이다.
또한 지속적으로 고객의 요구가 변경된다면 ScoreRecord 클래스는 계속 변경해주어야 할 것이다.
<br/>
문제 해결의 핵심은 성적 통보대상이 변경되더라도 ScroeRecord 클래스는 그대로 재사용 할 수 있어야 한다는 점이다.
개발자는 고심끝에 디자인 패턴중 옵저버 패턴을 사용하기로 결정한다.
다음과 같이 클래스 구조를 설계하였다.
<img src="https://static.podo-dev.com/blogs/images/2019/07/10/origin/DRVO1F181224235509.PNG">
ScoreRecord는 다수의 Observer를 참조한다.
몇개의 Observer가 추가되더라도 참조하는 N개의 Observer에 update()를 요청한다면,
고객의 어느 추가적인 출력요청에도 ScoreRecord 클래스는 변하지 않을 것이다.
<br/>
다음과 같이 구현하였다.
public class ScoreRecord {
private List<Integer> scores = new LinkedList<>();
private List<Observer> observers = new LinkedList<>();
public void addObserver(Observer observer) {
observers.add(observer);
}
public void addScore(int s) {
scores.add(s);
for (int i = 0; i < observers.size(); i++) {
observers.get(i).update();
}
}
public List<Integer> getScores() {
return scores;
}
}
public interface Observer {
public abstract void update();
}
<br/>
public class DataSheetView implements Observer {
private ScoreRecord scoreRecord;
private int n;
public DataSheetView(ScoreRecord scoreRecord, int n) {
this.scoreRecord = scoreRecord;
this.n = n;
}
public void update() {
List<Integer> scores = scoreRecord.getScores();
displayScores(scores);
}
private void displayScores(List<Integer> scores) {
System.out.print(n + "개 출력 SHEET ");
for (int i = 0; i < n && i < scores.size(); i++) {
System.out.print(scores.get(i) + " ");
}
System.out.println("");
}
}
<br/>
public class MinMaxSheetView implements Observer{
private ScoreRecord scoreRecord;
public MinMaxSheetView(ScoreRecord scoreRecord) {
this.scoreRecord = scoreRecord;
}
public void update() {
List<Integer> scores = scoreRecord.getScores();
displayScores(scores);
}
private void displayScores(List<Integer> scores) {
System.out.print("최소값, 최대값 출력 SHEET ");
System.out.print(Collections.min(scores) + " ");
System.out.print(Collections.max(scores) + " ");
System.out.println("");
}
}
<br/>
public class Main {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
scoreRecord.addObserver(new DataSheetView(scoreRecord, 1));
scoreRecord.addObserver(new DataSheetView(scoreRecord, 2));
scoreRecord.addObserver(new DataSheetView(scoreRecord, 3));
scoreRecord.addObserver(new DataSheetView(scoreRecord, 4));
scoreRecord.addObserver(new DataSheetView(scoreRecord, 5));
scoreRecord.addObserver(new MinMaxSheetView(scoreRecord));
scoreRecord.addScore(10);
scoreRecord.addScore(20);
scoreRecord.addScore(30);
scoreRecord.addScore(40);
scoreRecord.addScore(50);
}
}
<br/>
출력
1개 출력 SHEET 10
2개 출력 SHEET 10
3개 출력 SHEET 10
4개 출력 SHEET 10
5개 출력 SHEET 10
최소값, 최대값 출력 SHEET 10 10
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20
4개 출력 SHEET 10 20
5개 출력 SHEET 10 20
최소값, 최대값 출력 SHEET 10 20
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30
5개 출력 SHEET 10 20 30
최소값, 최대값 출력 SHEET 10 30
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30 40
5개 출력 SHEET 10 20 30 40
최소값, 최대값 출력 SHEET 10 40
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30 40
5개 출력 SHEET 10 20 30 40 50
최소값, 최대값 출력 SHEET 10 50
<br/>
로직이 정상적으로 작동하였고, 출력결과도 정확하였다.
또한 앞으로의 추가적은 고객의 요구에도, ScoreRecord 클래스의 변동은 없을 것이다.
<br/>
사실 개발자는 한번의 일반화를 더해 좀 더 아름다은 구조로 만들려한다.
이런 옵저버 패턴을 여러 곳에서 사용 한다고 해보자, 좀더 일반화 해보는 것이다.
개발자는 다음과 같이 클래스 구조를 변경하였다.
<img src="https://static.podo-dev.com/blogs/images/2019/07/10/origin/PUXYKC181224235509.PNG">
Subject 클래스를 설계함으로써, 옵저버패턴을 일반화하였다.
옵저버패턴을 사용하고 싶다면, Subject, Observer를 상속하는 것으로 쉽게 구현 할 수 있다.
<br/>
다음과 같이 구현하였다.
public class Subject {
private List<Observer> observers = new LinkedList<>();
public void attach(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
observers.get(i).update();
}
}
}
public class ScoreRecord extends Subject {
private List<Integer> scores = new LinkedList<>();
public void addScore(int s) {
scores.add(s);
notifyObservers();
}
public List<Integer> getScores() {
return scores;
}
}
<br/>
public class Main {
public static void main(String[] args) {
ScoreRecord scoreRecord = new ScoreRecord();
scoreRecord.attach(new DataSheetView(scoreRecord, 1));
scoreRecord.attach(new DataSheetView(scoreRecord, 2));
scoreRecord.attach(new DataSheetView(scoreRecord, 3));
scoreRecord.attach(new DataSheetView(scoreRecord, 4));
scoreRecord.attach(new DataSheetView(scoreRecord, 5));
scoreRecord.attach(new MinMaxSheetView(scoreRecord));
scoreRecord.addScore(10);
scoreRecord.addScore(20);
scoreRecord.addScore(30);
scoreRecord.addScore(40);
scoreRecord.addScore(50);
}
}
<br/>
타 클래스는 기존과 동일하다
<br/>
출력
1개 출력 SHEET 10
2개 출력 SHEET 10
3개 출력 SHEET 10
4개 출력 SHEET 10
5개 출력 SHEET 10
최소값, 최대값 출력 SHEET 10 10
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20
4개 출력 SHEET 10 20
5개 출력 SHEET 10 20
최소값, 최대값 출력 SHEET 10 20
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30
5개 출력 SHEET 10 20 30
최소값, 최대값 출력 SHEET 10 30
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30 40
5개 출력 SHEET 10 20 30 40
최소값, 최대값 출력 SHEET 10 40
1개 출력 SHEET 10
2개 출력 SHEET 10 20
3개 출력 SHEET 10 20 30
4개 출력 SHEET 10 20 30 40
5개 출력 SHEET 10 20 30 40 50
최소값, 최대값 출력 SHEET 10 50
<br/>
<br/>
<br/>