Pv_log

9. 옵서버 패턴 본문

Develop Study/Design Pattern

9. 옵서버 패턴

Priv 2022. 7. 3. 23:11


 

 

1. 여러 방법으로 성적 출력하기

입력된 성적 값을 출력하는 프로그램을 작성한다고 가정하자.

입력된 성적을 저장하는 ScoreRecord 클래스, 점수 목록을 출력하는 DataSheetView 클래스가 필요하다.

ScoreRecord 클래스의 addScore 메서드가 실행될 때는 성적을 출력하기 위해 ScoreRecord 클래스가 DataSheetView 객체를 참조해야 한다.

ScoreRecord 클래스의 addScore 메서드가 호출되면, ScoreRecord 클래스는 자신의 필드인 scores 객체에 점수를 추가한다.

그런 다음, DataSheetView 클래스의 update 메서드를 호출함으로써 성적을 출력하도록 요청한다.

DataSheetView 클래스는 ScoreRecord 클래스의 getScoreRecord 메서드를 호출해 출력할 점수를 구한다.

이때 DataSheetView 클래스의 update 메서드에서는 구한 점수 중, 명시된 개수(viewCount 변수)만큼 점수를 출력한다.

package P9;

import java.util.ArrayList;
import java.util.List;

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

}
package P9;

import java.util.List;

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

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

 


 

2. 문제점

만약 성적을 다른 형태로 출력하고 싶다면 어떻게 변경해야 하는가?

여러 형태의 성적을 동시/순차적으로 출력하려면 어떻게 해야 하는가?

 

2.1) 성적을 다른 형태로 출력하는 경우

점수가 입력되면 점수 목록을 출력하는 대신, 최소/최대 값만 출력하도록 만든다고 가정하자.

이제 기존의 DataSheetView 클래스 대신 최소/최대 값만 출력하는 MinMaxView 클래스를 추가해야 한다.

그리고 ScoreRecord 클래스는 DataSheetView 클래스가 아니라 MinMaxView 클래스에 성적 변경을 통보해야 한다.

package P9;

import java.util.ArrayList;
import java.util.List;

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

    public void setDataSheetView(MinMaxView minMaxView) {
        this.minMaxView = minMaxView;
    }
    
    public void addScore(int score) {
        scores.add(score);
        this.minMaxView.update();
    }

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

}
package P9;

import java.util.Collections;
import java.util.List;


public class MinMaxView {
    private ScoreRecord scoreRecord;

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

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

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

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


}

이제 MinMaxView 클래스의 update 메서드는 ScoreRecord 클래스의 getScoreRecord 메서드로 구한 점수를 바탕으로 displayMinMax 메서드를 호출해 최소/최대 값을 출력한다.

기존 ScoreRecord 클래스의 addScore 메서드를 직접 변경해서 MinMaxView 클래스의 update 메서드를 호출하는 방식으로 구현하였다.

다만 이러한 구현 방식은 OCP에 위배되는 방식이다.

여기서 다른 대상에게 점수가 입력되었음을 통보하려면 ScoreRecord 클래스의 변경이 불가피하기 때문이다.

 

2.2) 동시 혹은 순차적으로 성적을 출력하는 경우

성적이 입력되었을 때 최대 3개 목록, 최대 5개 목록, 최소/최대 값을 동시에 출력하거나 처음에는 목록으로 출력하고, 나중에 최소/최대 값을 출력하도록 바꿔보자.

목록을 출력하는 기능은 DataSheetView 클래스를, 최소/최대 값을 출력하는 기능에는 MinMaxView 클래스를 사용한다.

즉, ScoreRecord 클래스는 2개의 DataSheetView 객체, 1개의 MinMaxView 객체에 성적 추가를 통보하도록 수정되어야 한다.

package P9;

import java.util.ArrayList;
import java.util.List;

public class ScoreRecord {
    private List<Integer> scores = new ArrayList<Integer>();
    private MinMaxView minMaxView;
    private List<DataSheetView> dataSheetViews = new ArrayList<DataSheetView>();

    public void setDataSheetView(MinMaxView minMaxView) {
        this.minMaxView = minMaxView;
    }
    
    public void addScore(int score) {
        scores.add(score);
        
        for (DataSheetView dataSheetView:dataSheetViews) {
            dataSheetView.update();
        }

        this.minMaxView.update();
    }

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

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

}
package P9;

import java.util.List;

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

    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();
    }
    
}
package P9;

import java.util.Collections;
import java.util.List;


public class MinMaxView {
    private ScoreRecord scoreRecord;

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

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

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

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


}

이 경우에도 ScoreRecord 클래스 코드를 수정하게 되기 때문에 OCP에 위반된다.

성적 변경을 새로운 클래스에 통보해야 할 때마다 수정이 이루어질 것이므로 상당히 번거로워질 수 있다.

 


 

3. 해결책

문제 해결의 핵심은 성적 통보 대상이 바뀌더라도 ScoreRecord 클래스의 코드를 수정하지 않는 것이다.

이를 위해서는 ScoreRecord 클래스에서 변화되는 부분을 식별, 이를 일반화해야 한다.

ScoreRecord 클래스는 통보 대상인 객체를 참조하는 것을 관리해야 한다.

addScore 메서드는 각 통보 대상인 객체의 update 메서드를 호출해야 한다.

이 공통 기능은 상위 클래스/인터페이스 형태로 일반화하고, 이를 이용해 ScoreRecord를 구현하는 것이 더 좋은 선택이다.

Subject 클래스는 성적 변경에 관심이 있는 대상 객체를 관리한다.

attach 메서드 detach 메서드는 성적 변경에 관심이 있는 대상 객체를 추가하거나 제거한다.

성적 변경 통보 수신이라는 측면에서 DataSheetView 클래스, MinMaxView 클래스는 동일하므로, Subject 클래스는 Observer 인터페이스로 성적 변경에 관심이 있음을 알려준다.

SocreRecord 클래스의 addScore 메서드가 호출되면 자신의 성적 값을 저장한 뒤, DataSheetView, MinMaxView 클래스에 성적 변경 통보를 위해 Subject 클래스 notifyObservers 메서드를 호출한다.

그러면 Subject 클래스가 Observer 인터페이스로 두 클래스의 객체의 update 메서드를 호출한다.

package P9;

public interface Observer {
    public abstract void update();
}
package P9;

import java.util.ArrayList;
import java.util.List;

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

    public void notifyObservers() {
        for (Observer o : observers){
            o.update();
        }
    }
}
package P9;

import java.util.ArrayList;
import java.util.List;

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


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

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

import java.util.List;

public class DataSheetView implements Observer {
    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.println("List of " + viewCount + " entries: ");

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

import java.util.Collections;
import java.util.List;


public class MinMaxView implements Observer {
    private ScoreRecord scoreRecord;

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

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

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

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


}

이제 성적 변경에 관심이 있는 객체들을 Subject 클래스에서 구현하고, ScoreRecord 클래스는 Subject 클래스를 상속받으므로, 코드 수정 없이 새로운 관심 클래스 및 객체 추가/삭제가 가능해졌다.

 


 

4. 옵서버 패턴

옵서버 패턴은 데이터 변경이 발생할 경우, 상대 클래스나 객체에 의존하지 않고 데이터 변경을 통보할 수 있는 패턴이다.

새로운 파일이 추가되거나, 기존 파일이 삭제되면 파일 탐색기는 이를 즉시 반영해서 표시해주어야 한다.

탐색기를 여러 개 켜 두고 파일 시스템을 한 탐색기에서 변경할 경우, 다른 탐색기에 이 변경 사항이 즉각 반영되도록 만들어주어야 할 것이다.

옵서버 패턴은 통보 대상 객체의 관리를 Subject 클래스, Observer 인터페이스로 일반화한다.

그러면 데이터 변경을 통보하는 클래스는 통보 대상 클래스, 객체에 대한 의존성이 사라진다.

즉, 옵서버 패턴은 통보 대상 클래스, 대상 객체의 변경에도 클래스 수정 없이 사용할 수 있도록 해주는 패턴이다.

옵서버 패턴을 성적 출력하기 예제에 적용하면 다음과 같다.

 


 


수고하셨습니다!


'Develop Study > Design Pattern' 카테고리의 다른 글

11. 템플릿 메서드 패턴  (0) 2022.07.03
10. 데커레이터 패턴  (0) 2022.07.03
8. 커맨드 패턴  (0) 2022.07.03
7. 스테이트 패턴  (0) 2022.07.03
6. 싱글턴 패턴  (0) 2022.07.03
0 Comments