본문 바로가기

Design pattern

[JAVA 디자인패턴] Observer pattern(옵저버 패턴)

데이터의 변경이 발생하였고 변경을 실시간으로 통보해야할 때, OCP를 만족시키면서 데이터의 변경을 통보하는 framework이다.

Observer 패턴을 이해하기 위해, 학생의 점수를 입력하고 최대/최소값을 보여주는 클래스를 작성해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ScoreRecord {
    private List<Integer> scores = new ArrayList<Integer>();
    private MinMaxView minMaxView;
 
    public void setMinMaxView(MinMaxView minMaxView) {
        this.minMaxView = minMaxView;
    }
 
    public List<Integer> getScores() {
        return scores;
    }
 
    public void addScore(int score){
        scores.add(score);
        minMaxView.update();
    }
    public void deleteScore(int idx){
        scores.remove(idx);
        minMaxView.update();
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MinMaxView {
    private ScoreRecord scoreRecord;
 
    public void setScoreRecord(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }
 
    public void update(){
        List<Integer> scores = scoreRecord.getScores();
 
        System.out.println("max : " + max +" min : " + min);
 
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
    public static void main(String[] args) {
        ScoreRecord scoreRecord = new ScoreRecord();
        MinMaxView minMaxView = new MinMaxView();
 
        scoreRecord.setMinMaxView(minMaxView);
        minMaxView.setScoreRecord(scoreRecord);
 
        scoreRecord.addScore(100);
        scoreRecord.addScore(80);
        scoreRecord.addScore(70);
        scoreRecord.addScore(60);
        scoreRecord.addScore(40);
 
        scoreRecord.deleteScore(0);
 
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

 

ScoreRecord는 점수를 저장하는 클래스이다. 그리고 ScoreRecord의 addScore를 통해서 점수를 입력하면 내부에 컬렉션에 저장된다.

 

MinMaxView 클래스를 통해서 ScoreRecord의 최대 최소값을 출력해준다. 여기서 중요한 것은, 데이터가 추가/삭제 될때마다 데이터의 현황을 보여준다. 여기서 중요한 것이, 우리가 원하는 기능은 데이터가 변할 때마다 데이터의 현황을 실시간으로 보여주는 기능이다. 병원에서 대기번호를 보면서 기다리는 것을 생각해보자. 누군가 병원에 와서 자신의 이름을 간호사님께 알려드리면, 모니터에 자신의 이름이 올라온다. 그리고 진료를 마치면 이름이 삭제된다. 이렇게 실시간으로 데이터가 입력되고 삭제되는 화면을 원하는 상황이다.

 

1
2
3
4
5
6
max : 100 min : 100
max : 100 min : 80
max : 100 min : 70
max : 100 min : 60
max : 100 min : 40
max : 80 min : 40

 

데이터가 추가될 때마다 최대 최소값을 보여주고 있다. 처음 입력된 100은 컬렉션에 최초로 입력된 값이기 때문에 최대 최소가 모두 100이다. 하지만 점차 데이터가 추가면서 최소값이 변한다. 마지막에 100을 삭제했기 때문에 max가 80으로 바뀐다.

 

항상 그랬던 것처럼, 이와같은 설계가 잘못된 것이 있는지 확인해보자. 클래스가 추가되는 상황을 가정해보면 된다. 평균값을 출력하는 클래스가 추가된다고 생각해보자.

 

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
public class ScoreRecord {
    private List<Integer> scores = new ArrayList<Integer>();
    private MinMaxView minMaxView;
    private AverageView averageView; //추가되는 코드
 
 
    public void setMinMaxView(MinMaxView minMaxView) {
        this.minMaxView = minMaxView;
    }
 
    //추가되는 코드
    public void setAverageView(AverageView averageView) {
        this.averageView = averageView;
    }
 
    public List<Integer> getScores() {...}
 
    public void addScore(int score){
        scores.add(score);
        minMaxView.update();
        averageView.update();//추가되는 코드
    }
    public void deleteScore(int idx){
        scores.remove(idx);
        minMaxView.update();
        averageView.update(); //추가되는 코드
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AverageView {
    private ScoreRecord scoreRecord;
 
    public void setScoreRecord(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }
 
    public void update(){
        List<Integer> scores = scoreRecord.getScores();
        System.out.println(ave);
 
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
    public static void main(String[] args) {
        ScoreRecord scoreRecord = new ScoreRecord();
        MinMaxView minMaxView = new MinMaxView();
 
        scoreRecord.setMinMaxView(minMaxView);
        minMaxView.setScoreRecord(scoreRecord);
 
        AverageView averageView = new AverageView();
        scoreRecord.setAverageView(averageView);
        averageView.setScoreRecord(scoreRecord);
 
        //....add/delete
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

 

1
2
3
4
5
6
7
8
9
10
11
12
max : 100 min : 100
100.0
max : 100 min : 80
90.0
max : 100 min : 70
83.33333333333333
max : 100 min : 60
77.5
max : 100 min : 40
70.0
max : 80 min : 40
62.5

 

AverageView 클래스를 추가하면, ScoreRecord의 멤버변수로 AverageView를 추가해줘야 한다. 즉, ScoreRecord의 코드가 수정되어 버린다. OCP의 핵심 원칙을 위배하고 있다. 새로운 기능이 추가되었는데, 다른 클래스에 수정되면 안된다. OCP를 만족시키면서 데이터의 변경을 실시간으로 통보하는 디자인 패턴이 Observer 디자인 패턴이다.

 

이제 Observer 패턴을 구현하는 과정에 대해서 알아보자. 이전 디자인패턴 글에서 보았듯, OCP를 만족시키지 않으면 일반화된 개념을 만들어서 해당 개념을 재구현하는 방식으로 하였다. 그렇게 구현해보자.

 

strategy pattern에서도 본것과 같은 구조이다. View에 대한 일반화된 개념(Observer)을 만들고, ScoreRecord는 그 일반화된 개념들을 멤버변수로 가진다. 여기서는 전략 패턴과 다르게 Observer라는 일반화된 개념도 ScoreRecord를 멤버로 가져야한다. 왜냐하면 Observer가 출력해야하는 대상을 알아야 하기 때문이다. 옵저버가 무엇을 출력해야하는 지를 당연히 알아야 한다. ScoreRecord는 당연히 Observer를 멤버로 가져야한다. 왜냐하면 Observer에게 데이터가 추가/수정되었다는 것을 알려줘야하기 때문이다. 이는 전략 패턴에서 했던 것과 동일히다. 마지막으로 *가 붙은 이유는, 다양한 Observer를 가질 수 있기 때문이다. 위에서 MinMax와 Average 2개를 이용하는 상황을 생각하면 된다.

 

위의 설계는 괜찮아 보인다. 물론 OCP도 만족한다. 하지만 여기서 몇 가지 생각해보아야 하는 문제들이 있다.

첫 번째로, attach, detach, notify는 각각 Observer를 추가하고 삭제하고 Observer에게 데이터 추가/삭제를 알려주는 기능이다. 그러한 기능은 ScoreRecord에 구현된 상황이다. 그런데, 다른 Record, 예로 들어, 환자 데이터 정보가 있는 Record라면 해당 클래스도 attach, detach, notify를 구현해야 한다. 이는 상당히 번거러운 일이다. 그렇다면, 부모 클래스를 하나 만들고 Observer가 필요한 Record들은 부모 클래스를 extend하면 된다.

 

ScoreRecord에 있는 observer와 관련된 메소드들을 Subject라는 부모 클래스로 만들었다. 그러면 다른 Record들을 만들 때 Subject 클래스를 extend해서 구현하면 된다.

 

클래스 다이어그램의 연관관계가 바뀌었다. 기존에는 ScoreRecord가 Observer를 멤버로 가져야 했다. 왜냐하면 옵저버에게 score정보가 수정되었다는 것을 notify해줘야 하기 때문이다. 하지만, Observer를 관리하는 모든 메소드들을 Subject 클래스로 만들었기 때문에 Observer를 멤버로 가지는 클래스를 Subject 클래스로 바꿔주면 된다.그래서, Observer와 ScoreRecord의 양방향 관계가 단방향 관계로 바뀌었다. 기존에 있던 ScoreRecord에서 Observer의 방향은 Subject를 통해서 간접적으로 표현된다.

 

이 문제를 SRP의 관점에서도 생각해볼만하다. ScoreRecord는 Score를 관리하는 클래스이다. 그런데 해당 클래스에 Observer를 관리하는 메소드들이 추가로 있으면 여러 기능(책임)을 담당하게 된다. ScoreRecord는 점수를 관리하는 클래스로 설계했는데 다른 기능이 있어버린다. 그래서 Subject 클래스를 만들어서 Observer의 기능을 담당하도록 만든다. 그러면 Record 클래스는 Subject 클래스를 extends해서 구현하면 된다.

 

여담이지만, 여기서 한 가지 헷갈릴 수 있는 부분이 있다. Subject 클래스를 extends해서 ScoreRecord를 구현한다고 하자. 그러면 ScoreRecord는 attach, detach, notify에 접근할 수 있다. 그런데 접근할 수 있으면 해당 기능은 ScoreRecord가 가진게 아닌가라고 생각 할수도 있다. 이것은 잘못되었다. 3가지 Observer와 관련된 메서드들이 public으로 선언해서 자식클래스가 부모클래스의 기능에 접근 할 수 있는 것이다. 순수 자식 클래스의 기능이 아니다. 접근의 권한이 있을 뿐이다.

 

두번 째로 생각해봐야하는 문제가 있다. Observer와 ScoreRecord의 연관관계이다. 이를 해결하기 위해서, 프레임워크의 입장에서 생각해보자. 프레임워크는 기본적인 뼈대만을 제공하면 된다. 구체적인 코드를 작성하게 되면 결합도가 증가하게 된다. 하지만 위의 클래스 다이어그램과 같이, Observer 프레임 워크에 특정 애플리케이션 개발자가 작성한 ScoreRecord가 있는 건 말이 안된다. 이러한 설계는 프레임워크 관점에서 말도 안되는 설계이다. 다시 말하지만, 프레임워크는 기본이 되는 뼈대만 제공하면 된다.

 

 

(Subject와 Observer는 프레임워크의 단위로 보아야한다)

애플리케이션 개발자가 Observer 패턴을 구현해야 하는 상황을 생각해보자. 애플리케이션 개발자는 Subject를 부모 클래스로 두어서 Record를 구현하면 되고, Observer 인터페이스를 구현해서 MinMaxView와 같은 View 방식들을 구현하면 된다. Observer 인터페이스와 Subject 클래스는 프레임워크 개발자가 구현한 것이다.

 

다시 말하지만, Observer는 프레임워크이기 때문에 구체적인 코드가 있어서는 안된다. 그래서 Observer 인터페이스를 구현한 클래스(AverageView, MinMaxView)가 관심대상인 Record를 멤버로 가지게 하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ScoreRecord extends Subject {
    private List<Integer> scores = new ArrayList<Integer>();
 
    public List<Integer> getScores() {
        return scores;
    }
 
    public void addScore(int score){
        scores.add(score);
        notifyObservers();
    }
    public void deleteScore(int idx){
        scores.remove(idx);
        notifyObservers();
    }
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Subject {
    private List<Observer> observers = new ArrayList<Observer>();
 
    public void attach(Observer observer){
        observers.add(observer);
    }
 
    public void detach(Observer observer){
    }
 
    public void notifyObservers(){
        observers.forEach(Observer::update);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
public interface Observer {
    void update();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AverageView implements Observer {
    private ScoreRecord scoreRecord;
 
    public void setScoreRecord(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }
 
    public void update(){
        List<Integer> scores = scoreRecord.getScores();
        System.out.println(ave);
 
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MinMaxView implements Observer {
    private ScoreRecord scoreRecord;
 
    public void setScoreRecord(ScoreRecord scoreRecord) {
        this.scoreRecord = scoreRecord;
    }
 
    public void update(){
        List<Integer> scores = scoreRecord.getScores();
 
        System.out.println("max : " + max +" min : " + min);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
    public static void main(String[] args) {
        ScoreRecord scoreRecord = new ScoreRecord();
 
        MinMaxView minMaxView = new MinMaxView();
        AverageView averageView = new AverageView();
        minMaxView.setScoreRecord(scoreRecord);
        averageView.setScoreRecord(scoreRecord);
 
 
        scoreRecord.addScore(100);
        scoreRecord.addScore(80);
        scoreRecord.addScore(70);
        scoreRecord.addScore(60);
        scoreRecord.addScore(40);
 
        scoreRecord.deleteScore(0);
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

 

참고문헌

[JAVA 객체지향 디자인 패턴, 정인상]