일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 |
- scanner
- 자바입력
- input
- 사칙연산
- array
- R
- JAVA11
- Easy
- SpringBoot 2
- 수학
- hash table
- 카데인 알고리즘
- heroku
- 자바 스레드 실행 순서 제어
- 자바 thread 실행 순서 제어
- Kadane's Algorithm
- Today
- Total
DeFacto-Standard IT
Observer Pattern 본문
1. 가정
- 숫자를 저장하는 DB를 만든다. (클래스로 한정)
- DB에 있는 숫자 중 최소, 최대 값을 출력하는 차트를 만든다.
- 차트는 DB에 값이 추가될 때 마다 추가된 값 까지 고려하여 최소, 최대값을 재출력한다.
2. Naive Code
- NumberDataBase
public class NumberDataBase {
private List<Integer> numberList;
// DB에 값이 입력될 때 마다 Chart를 갱신시키기 위해 레퍼런스 존재
private MinMaxChart minMaxChart;
public NumberDataBase(MinMaxChart minMaxChart){
numberList = new ArrayList<Integer>();
this.minMaxChart = minMaxChart;
}
public void insertNumber(int number) {
numberList.add(number);
// 값이 들어올 때 마다 차트를 출력
this.minMaxChart.update();
}
public List<Integer> select() {
return numberList;
}
}
숫자를 저장하는 DB이다. List를 사용하여 숫자를 저장하고 있다. insertNumber는 DB에 숫자를 저장하며, 저장 할 때 마다 Chart를 보여주게 해야하므로 Chart에 대한 레퍼런스를 가지고 있다. select는 현재 DB의 모든 값 들을 Chart에 넘긴다.
- MinMaxChart
public class MinMaxChart {
// DB로부터 값을 전달받기 위해 레퍼런스 존재
NumberDataBase numberDataBase;
public void setNumberDataBase(NumberDataBase numberDataBase) {
this.numberDataBase = numberDataBase;
}
public void update() {
List<Integer> numberList = numberDataBase.select();
show(numberList);
}
public void show(List<Integer> numberList) {
System.out.println("min : " + Collections.min(numberList));
System.out.println("max : " + Collections.max(numberList));
}
}
최소, 최대값을 출력하는 Chart이다. DB로부터 값을 넘겨 받아 show()를 이용해 최소, 최대값을 출력한다. update()는 DB에 값이 들어간 직후 호출되어 새로운 리스트를 받아, 해당 Chart에서 지원하는 기능(최소, 최대값 출력)을 수행한다
- NaiveClient
public class NaiveClient {
public static void main(String args[]) {
MinMaxChart minMaxChart = new MinMaxChart();
NumberDataBase numberDataBase = new NumberDataBase(minMaxChart);
minMaxChart.setNumberDataBase(numberDataBase);
numberDataBase.insertNumber(3);
System.out.println();
numberDataBase.insertNumber(2);
System.out.println();
numberDataBase.insertNumber(4);
System.out.println();
numberDataBase.insertNumber(1);
System.out.println();
numberDataBase.insertNumber(10);
}
}
Client는 Chart와 DB를 정의하고, DB에 값을 넣는다.
Chart와 DB가 양방향 연관관계(Bidirection)을 갖는 이유는 다음과 같다.
DB 입장 : 값이 입력될 때 마다 Cart에게 이를 알려야 함
Chart 입장 : DB로부터 저장되어있는 값들을 제공받아야 함
실행 결과는 다음과 같다.
3. 문제점
위 소스코드는 다음과 같은 문제점이 존재한다.
1. 최소/최대 값을 출력하는 형태가 아닌, 다른 Chart를 사용하는 경우
현재는 성적이 입력되면, 최소/최대값을 출력하는 MinMaxChart가 NumberDataBase로부터 통보를 받고 이를 갱신하여 재출력한다.
만약, 현재 최소/최대 차트 출력이 아니라 오름차순/내림차순 정렬의 결과를 보여주도록 변경한다면,
AscChart 혹은 DescChart 등의 새로운 차트를 구현하고, NumberDataBase에 대한 레퍼런스를 변경해야한다.
즉, 기존의 소스의 수정이 동반되어 OCP를 위반한다.
2. 여러 개의 Chart를 동시에 출력하는 경우 또는 이 여러개의 Chart들 간에 출력 순서를 제어하는 경우
NumberDataBase는 Chart에 대한 레퍼런스를 가지고있다.
만약, 여러 개의 Chart를 사용한다면, Chart가 하나 추가될 때 마다 이 Chart에 통보를 해야하기 때문에 NumberDataBase의 변경은 불가피하다. 따라서 이 경우도 OCP를 위반한다.
4. 해결방안
NumberDataBase는 통보의 대상이 되는 각 Chart들에 대한 레퍼런스가 있어야 한다.
그리고 NumberDataBase의 insertNumber()에서는 값이 추가된 직후, 정보의 변경이 일어났기 때문에 이를 통보대상들에게 알려야 한다.
(update())
문제점 1, 2번의 문제가 생기는 이유는, NumberDataBase와 ConcreteChart들 간의 결합도 때문에 발생한다.
이를 해소하기 위해 ConcreteChart에 대한 추상적 개념이 필요하다.
추상적 개념을 만들때는, 조금만 생각을 하여 구현한다면 효율적이면서 짧은 소스코드가 만들어진다.
NumberDataBase는 각 차트들의 레퍼런스를 통해 update()라는 메서드를 수행시킨다.
왜냐면, 값이 입력될 때 마다 차트들이 이를 통보받고 출력해야하기 때문이다.
따라서, 차트들의 레퍼런스를 통해 update()를 수행한다는 것은 모든 차트들에게 공통된 사항이 된다.
따라서 추상적 개념에서 update()를 공통기능으로서 정의한다면 일관성있는 코드와 더불어 update()에 있어서는 코드의 변경이 필요가 없게된다.
추상적 개념을 선언하였기 때문에, 이를 일관된 관점으로 관리할 수 있다.
이러한, 정보의 변경을 통보받는 클래스에 대한 추상적인 개념을 옵저버 패턴에서는 'Observer'라고 한다. 예제에서는 Chart에 해당한다.
* 간혹가다가 Observer의 단어의 의미, 또는 스타크래프트에서 Observer는 내가 움직이기 때문에
정보의 변경을 '탐지하는' 능동적인 요소라고 오해할 수 있다.
정보의 변경을 '통보받는' 수동적인 요소라고 보는 것이 이해하기 쉽다.
왜냐면, NumberDataBase의 insertNumber()에서 Observer의 객체를 활용해 update()로 알려주기 때문이다.
그리고 각 기능을 구현한 차트 각각을 'ConcreteObserver'라고 한다. 예제에서는 MinMaxChart, AscChart, DescChart에 해당한다.
NumberDataBase의 경우 Observer에게 통보를 하는 역할을 한다. 즉, Observer들을 관리한다고 볼 수 있다.
NumberDataBase는 정보의 실질적인 변경이 이루어지며, 정보의 변경이 일어날 때 마다 Observer들은 이를 통보받고 자신이 할 일을 한다.
즉, NumberDataBase는 Observer의 입장에서는 '구체적인 상태변경 감시대상'이라고 할 수 있다.
여기서 한가지 더 생각해보자. 현재 NumberDateBase의 경우 크게 2가지 기능을 한다.
1. 정보의 변경( insertNumber() )
2. Observer들에게 정보의 변경을 알림 ( update() )
여기서 NumberDataBase의 가장 기본적인 역할은 1번이다.
2번의 경우는, NumberDataBase가 수행할 수 있는 일이기는 하지만, 다른 클래스가 하는 것이 더 좋다. 그 이유는 다음과 같다.
만약, 정보 변경에 대한 감시대상이 늘어난다고 해보자.
NumberDataBase는 숫자를 관리하지만 숫자 외에 문자를 관리하는 CharacterDataBase가 있을 수 있다.
그리고 이 역시도 정보의 변경이 일어날 때 마다 임의의Chart로 어떠한 연산을 해야한다고 생각해보자.
이때 CharacterDataBase를 구현할 때, NumberDataBase에서 Observer들을 관리하는 소스코드를 그대로 다시 작성하여 만들어야 한다.
왜냐하면, Observer에 대한 관리 자체를 NumberDataBase에서 수행하는 기능이기에, 사용할 수 없기 때문이다.
즉, 다른 클래스가 해야할 일을 자신(NumberDataBase)이 수행하면, 비슷한 역할을 하는 클래스(CharacterDataBase)는 이 소스코드를 재사용할 수 없다. 자신도 사용하긴 해야하기 때문에 또 작성하기 때문에 소스코드의 중복이 발생하는 것이다.
이는 SRP에 위배되어 발생하는 소스코드의 중복이다.
따라서 이 2개의 DataBase에서 수행하는 공통적인 기능을 추상화된 클래스에서 다룰 필요가 있다.
Observer패턴에서는 이것을 'Subject'라고 한다.
즉, Number/Character DataBase는 이 Subject 클래스를 상속받고, 이 2개의 DB는 'ConcreteSubject'라는 것으로 분류가 된다.
Subject 클래스에는 ConcreteSubject의 공통적인 기능인 Observer의 관리와 통보 등과 관련된 기능이 정의된다.
즉, Subject클래스는 옵저버를 관리하는 클래스가 된다.
위 설명을 바탕으로 코드를 작성하면 다음과 같은 소스코드가 나오게 된다.
5. Solution Code
- Observer
public interface Observer { // 추상화된, 정보의 변경을 통보 받는 대상
public void update(); // 데이터의 변경을 통보받았을 때 각각 처리하는 메서드
}
정보의 변경을 통지받는 ConcreteObserver(예제에서의 MinMax, ASC, DESC Chart)의 추상적인 개념이다. 정보의 변경이 일어나면 외부(Subject)에서 Observer Interface의 update()를 호출하여 정보의 변경을 통지받으므로 공통된 메서드가 존재한다.
- ConcreteObserver 1~3
// MinMaxChart
public class ConcreteObserver1 implements Observer {
private ConcreteSubject concreteSubject;
public ConcreteObserver1(ConcreteSubject concreteSubject) {
this.concreteSubject = concreteSubject;
}
public void update() { // 정보의 변경을 통보받음
List<Integer> numberList = concreteSubject.getNumbers(); // 변경된 데이터를 가져옴
showMinMAx(numberList); // 최소, 최대값 출력
}
private void showMinMAx(List<Integer> numberList) {
System.out.println("min : " + Collections.min(numberList));
System.out.println("max : " + Collections.max(numberList));
}
}
// AscChart
public class ConcreteObserver2 implements Observer {
private ConcreteSubject concreteSubject;
public ConcreteObserver2(ConcreteSubject concreteSubject) {
this.concreteSubject = concreteSubject;
}
public void update() { // 정보의 변경을 통보받음
List<Integer> numberList = concreteSubject.getNumbers(); // 변경된 데이터를 가져옴
showAsc(numberList); // 오름차순 출력
}
private void showAsc(List<Integer> numberList) {
Collections.sort(numberList);
System.out.print("ASC : ");
for(Integer number : numberList)
System.out.print(number + " ");
System.out.println();
}
}
// DescChart
public class ConcreteObserver3 implements Observer {
private ConcreteSubject concreteSubject;
public ConcreteObserver3(ConcreteSubject concreteSubject) {
this.concreteSubject = concreteSubject;
}
public void update() { // 정보의 변경을 통보받음
List<Integer> numberList = concreteSubject.getNumbers(); // 변경된 데이터를 가져옴
showDesc(numberList); // 내림차순 출력
}
private void showDesc(List<Integer> numberList) {
Collections.reverse(numberList);
System.out.print("DESC : ");
for(Integer number : numberList)
System.out.print(number + " ");
System.out.println();
}
}
정보의 변경이 일어날 때 마다 이를 통보받고, 실질적으로 정보의 변경이 일어나는 ConcreteSubject로 부터 정보를 제공받아 적절한 로직(showXXX())을 수행하는 클래스이다.
- Subject
// 추상화된 Observer들을 관리하는 클래스
// ConcreteSubject의 SRP를 위해 추상적인 개념으로 분리 -> 확장성 용이
public abstract class Subject {
// 추상화된 통보 대상(Observers) 목록
private List<Observer> observers = new ArrayList<Observer>();
// 옵서버(통보 대상) 추가
public void attach(Observer observer) {
observers.add(observer);
}
// 옵서버(통보 대상) 삭제
public void detach(Observer observer) {
observers.remove(observer);
}
// 등록된 observers(통보 대상들)에게 정보의 변경을 통보
public void notifyObservers() {
for (Observer observer : observers)
observer.update();
}
}
ConcreteSubject에서 구현하는, Observer 관리와 관련된 메서드들을 Subject라는 클래스에 구현하여 SRP를 만족시킨다.
추후에 ConcreteSubject가 여러개가 된다면, 이 Subject를 상속시킴으로써 재사용할 수 있다.
- ConcreteSubject
// 실질적으로 정보의 변경이 일어나는 클래스. -> 구체적인 변경 감시 대상 데이터
public class ConcreteSubject extends Subject {
private List<Integer> numbers = new ArrayList<Integer>();
// 실질적인 정보의 변경
public void addNumber(int number) {
numbers.add(number);
// 데이터가 변경되면 Subject 클래스의 notifyObservers 메서드를 호출해
// 각 옵서버(통보 대상 클래스)에게 데이터의 변경을 통보함
notifyObservers();
}
public List<Integer> getNumbers() {
return numbers;
}
}
실질적으로 정보의 변경이 일어난다. 예제에서 Number/Character DataBase에 해당한다.
정보의 변경이 일어나는 부분(addNumber)에서 연산 후에 Subject 클래스로부터 상속받은 메서드(notifyObservers())를 이용해 Observer들에게 정보의 변경을 통보한다.
이를 통보받은 Observer들은 ConcreteSubject로부터 정보를 요청(getNumbers())하고, 받은 정보(List<Integer> numbers)로 자신이 수행해야하는 로직(showXXX())을 수행한다.
6. Observer Pattern
옵서버 패턴은 통보 대상 객체의 관리를 Subject 클래스와 Observer 인터페이스로 일반화한다. 따라서 데이터 변경을 통보하는 클래스(ConcreteSubject)는 통보 대상(ConcreteObserver)에 대한 의존성을 없앨 수 있다. 결과적으로 통보 대상(ConcreteObserver)의 변경에도, ConcreteSubject 클래스의 수정없이 그대로 사용할 수 있다.
- UML & Sequence Diagram
- Observer : 데이터의 변경을 통보 받는 인터페이스. 즉, Subject에서는 Observer 인터페이스의 update 메서드를 호출함으로써 ConcreteSubject의 데이터 변경을 ConcreteObserver에게 통보한다.
- Subject : ConcreteObserver 객체를 관리하는 요소. Observer 인터페이스를 참조해서 ConcreteObserver를 관리하므로 ConcreteObserver의 변화에 독립적일 수 있다.
- ConcreteSubject : 변경 관리 대상이 되는 데이터가 있는 클래스, 데이터 변경을 위한 메서드인 setState가 있으며 setState에서는 자신의 데이터인 subjectState를 변경하고 Subject의 notifyObservers 메서드를 호출해서 ConcreteObserver 객체에 변경을 통보한다.
- ConcreteObserver : ConcreteSubject의 변경을 통보받는 클래스. Observer 인터페이스의 update 메서드를 구현함으로써 변경을 통보받는다. 변경된 데이터는 ConcreteSubject의 getState 메서드를 호출함으로써 변경을 조회한다.
ConcreteSubject가 자신의 상태, 즉 데이터의 변경을 통보하려면 ConcreteObserver가 미리 등록되어 있어야 한다.
변경 통보는 실제로는 ConcreteSubject의 상위 클래스인 Subject 클래스의 notifyObservers 메서드를 호출해 이루어진다. 등록된 모든 ConcreteObserver의 update 메서드를 호출하는 방식으로 알리게 된다.
ConcreteObserver는 데이터의 변경을 통보받았을 때 변경된 데이터를 신요청하기 위해 ConcreteSubject를 참조해야한다.
즉, ConcreteSubject와 ConcreteObserver는 양방향 연관관계(Bidirectional) 이다.
장점 : 데이터의 변경이 발생했을 경우 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보할 때 유용
단점 :
'Design Pattern > Behavioral Pattern' 카테고리의 다른 글
Iterator Pattern / Iterable Interface (0) | 2018.02.15 |
---|---|
Template Method Pattern (기본) (0) | 2017.09.26 |
Command Pattern (0) | 2017.09.26 |
State Pattern과 Strategy Pattern의 공통점과 차이점 (0) | 2017.09.26 |
State Pattern (0) | 2017.09.26 |