본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 옵저버 패턴(Observer Pattern)

observer 패턴은 통보 대상 클래스와 통보 하는 클래스 간의 의존성을 제거한다.

Context

모니터링 툴이나 통계 툴은 원본데이터가 변할 때마다 항상 그래프나 그리드에 해당 데이터를 반영하여 관제자가 볼 수 있도록 한다. 이제부터 학생의 성적에 관한 모니터링 툴을 만들 것이다. 일반적으로 생각했을 때는 아래와 같이 ScoreRecordDataSheetView를 서로 의존하여 ScoreRecordaddScore와 같은 점수 변경, 추가, 삭제에 대한 메서드라 호출될 때 DataSheetView의 객체에 변동되었다고 통보(update)해주면 된다.


먼저, DataSheetView클래스는 ScoreRecord 속성으로 가지고 있는데, 그냥 update(ScoreRecord:ScoreRecord)형식으로 실행하면 되는 것이 더 쉽지 않을까 생각할 수 있다. 보통 모니터링 툴 속의 특정 통계 그래프를 관찰하면 해당 통계(View)는 바라보고 있는 특정한 데이터의 집합이 있다. 따라서 통계뷰는 객체의 단위이며 특정 데이터 레코드에 대해서 의존관계를 가지고 있으므로 필드로 가지고 있어야한다.

만약 update(ScoreRecord:ScoreRecord)형식의 메서드라면 유틸성 메서드가 되므로 인스턴스 메서드보단 정적 메서드로 존재해야한다. 정적메서드로 존재하게 된다면 업데이트의 대상이 자기 자신인지 알 수 없다.

class ScoreRecord {
    private List<Integer> scores = new ArrayList<>();
    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;
    }
}

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);
    }

    private void displayScores(List<Integer> record, int viewCount) {
        System.out.print("List of " + viewCount + " entries: ");

        for (int i = 0; i < viewCount && i < record.size(); i++) {
            System.out.print(record.get(i) + " ");
        }
        System.out.println();
    }
}

Problem

변경에 대해서 안전한가

일단 바로 생각해봐도 View를 교체하는 것에 대해서 안전하지 않다. OCP를 만족할 수 없다. 구현한 View 클래스에 대해서 직접 의존하고 있기 때문에 RecordSocre의 수정은 불가피하다.

확장에 대해서 안전한가

기존의 DataSheetView뿐만 아니라 다른 뷰도 추가하고 싶다. 일단 먼저 MinMaxView를 추가해보자.

class MinMaxView {
    private ScoreRecord scoreRecord;

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

    public void update() {
        displayMinMax(scoreRecord.getScoreRecord());
    }

    private void displayMinMax(List<Integer> scores) {
        int max = Collections.max(scores, null);
        int min = Collections.min(scores, null);

        System.out.println("Min Max : " + min + ", " + max);
    }
}

그리고 ScoreRecord의 객체의 데이터가 변경될 때 역시 View에 통보를 해줘야하므로 ScoreRecord에 필드에 추가하고 addScoreupdate함수를 호출한다.

class ScoreRecord {
    private List<Integer> scores = new ArrayList<>();
    private DataSheetView dataSheetView;
    private MinMaxView minMaxView; // 추가

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

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

    public void addScore(int score) {
        scores.add(score);
        dataSheetView.update();
        minMaxView.update(); // 추가
    }

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

기능의 확장에 대해서 ScoreRecord를 수정했으므로 OCP에 위반한다, 확장에 대해서 안전하지 않다.

동일 클래스의 대상이 2개 이상의 경우에도 안전한가

정책이 다른 동일 타입의 View를 추가하고 싶다. 그러면 복수의 같은 인스턴스를 가지게 되므로 아래와 같이 변경해야한다. 그리고 새로운 View 추가될 수 록 변경해야하는 요소가 많아진다, MinMaxView 역시 복수개가 존재하게 된다면 변경을 해야한다.

class ScoreRecord {
    private List<Integer> scores = new ArrayList<>();

    private List<DataSheetView> dataSheetViews = new ArrayList<>();

    private MinMaxView minMaxView;

    public void addDataSheetView(DataSheetView dataSheetView) {
        dataSheetViews.add(dataSheetView);
    }

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

    public void addScore(int score) {
        scores.add(score);
        dataSheetViews.forEach(DataSheetView::update);
        minMaxView.update();
    }

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

Solution

위와 같은 문제는 우선적으로 View의 구현클래스에 직접적으로 ScoreRecord가 관계를 가지고 있어서 발생하는 문제이다. 다른 패턴처럼 ScoreRecord의 변화의 부분에 대해서 일반화 시키면 ScoreRecord는 일반화된 인터페이스를 의존하게 되고, 기능이 확장되더라고 기존 코드에는 아무런 수정을 하지 않는다.

먼저 데이터의 변경에 대해서 관찰하고 있는 클래스를 일반화하면 아래와 같다. 각 View들은 모두 update라는 공통적인 메서드을 가지고 있다.

interface Observer {
    public void update();
}

그리고 아래와 같이 구현한다.

class DataSheetView implements Observer {
    private ScoreRecord scoreRecord;
    private int count;

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

    @Override
    public void update() {
        displayDataSheet(scoreRecord.getScores());
    }


    public void displayDataSheet(List<Integer> scores) {
        StringBuffer stringBuffer = new StringBuffer("List of Scores : ");

        for(int i = 0; i < count && i < scores.size(); i++) {
            stringBuffer.append(scores.get(i) + " " );
        }

        System.out.println(stringBuffer.toString());
    }
}

class MinMaxView implements  Observer {
    private ScoreRecord scoreRecord;

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

    @Override
    public void update() {
        displayMinMax(scoreRecord.getScores());
    }

    public void displayMinMax(List<Integer> scores) {
        System.out.println("Min : " + Collections.min(scores) + ", Max : " + Collections.max(scores));
    }
}

통보 대상을 관리하고 통보시키는 역할을 추상화할 수 있다. 관리자는 Observer를 자신의 것으로 종속시킬 수 있기에 이름이 Subject로 붙은 것 같다.

abstract class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void attachObserver(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    protected void notifyObserver() {
        observers.forEach(Observer::update);
    }
}

Observer들을 붙히거나(attach) 때거나(detach) 이들에 변경에 대해서 통보(notifyObserver)한다. 하지만 Subject는 스스로 아무런 상태를 가지고 있지 않기 때문에 추상클래스로 인스턴스를 생성할 수 없게 한다.

class ScoreRecord extends Subject {
    private List<Integer> scores = new ArrayList<>();

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

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

ScoreRecordSubject를 상속받음으로서 스스로 Observer들을 관리할 수 있는 관리자가 된다. 그럼 여기서 하나의 View를 추가해보자.

class StatisticsView implements Observer{
    private ScoreRecord scoreRecord;

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

    @Override
    public void update() {
        displayStatistics(scoreRecord.getScores());
    }

    private void displayStatistics(List<Integer> scores) {
        scores.stream().reduce(Integer::sum).ifPresent((totalValue) -> {
            System.out.println("Sum : " + totalValue + ", Average : " + totalValue/scores.size());
        });
    }
}

그리고 이를 아래와 같이 사용할 수 있다.

public static void main(String[] args) {
    ScoreRecord scoreRecord = new ScoreRecord();

    scoreRecord.attachObserver(new StatisticsView(scoreRecord));

    for(int i = 0; i < 10; i ++) {
        scoreRecord.addScore((int)(Math.random()*100));
    }
}

기능을 확장했음에도 ScoreRecord는 아무런 변화가 없었다. 즉 OCP를 만족한다. 이를 다이어그램으로 간단히 표현하면 아래와 같다.


Summary

옵저버 패턴은 통보 대상 객체의 관리를 Subject 클래스와 Observer 인터페이스로 일반화하여, 실제 ConcreteSubjectConcreteObserver 클래스 간의 의존성을 제거한다. ConcreteObserver클래스의 파생클래스가 얼마든지 생겨도 결과적으로 ConcreteSubject코드는 변화가 없다.

역할

  • Observer : 데이터의 변경을 통보받는 인터페이스.
  • Subject : ConcreteObserver 객체를 관리하는 관리자
  • ConcreteSubject : 변경 관리 대상이 되는 데이터를 가지고 있는 클래스
  • ConcreteObserver : ConcreteSubject로부터 변경된 데이터를 통보받는 클래스