[Design Pattern] 감시자 패턴(Observer Pattern)

뚜비·2023년 3월 15일
0

Design Pattern

목록 보기
5/9

💻 감시자 패턴 구현 코드


감시자 패턴

"객체 사이에 일 대 다의 의존 관계를 정의해 두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 갱신될 수 있게 만듭니다."
-Gang Of Four-

  • 객체(Subject)의 상태 변화를 관찰하다가, 상태 변화가 있을 때마다 옵저버(Observer) 목록에 있는 옵저버들(Observers)에게 메서드 등을 통해 변화를 알려주는 디자인 패턴, 이때 변화는 자동으로 갱신

  • MVC(Model, View, Controller) 패턴에도 사용

🤔 어떻게 사용되나요?

  • 옵저버 패턴은 MVC에서 모델과 뷰 사이를 느슨히 연결하기 위해 사용
  • Model(표시 형식에 의존하지 않는 내부 모델을 조작) ↔ Subject
    View(Model이 어떻게 보일 것인지를 관리) ↔ Observer
    → 하나의 Model에 복수의 View가 대응
  • 주체인 모델(model)에서 일어나는 이벤트update() 메서드옵저버인 뷰에 알려주고 이를 기반으로 컨트롤러 등이 작동

🤔언제 사용하나요?
1. 어떤 객체의 변화로 인해 다른 객체가 변경되어야 하는데, 프로그래머들은 얼마나 많은 객체들이 변경되어야 하는지 몰라도 될 때
2. 외부에서 발생한 이벤트에 대해 결과를 처리해야 할 때
3. 이벤트 기반 시스템, 분산 이벤트 핸들링 시스템에 주로 사용

EX) 트위터
어떤 주체를 '팔로우'했다면 주체가 포스팅을 올릴 때(Subject의 상태변화 시) 알림이 팔로워(Observers)에게 가야함!!



구조

  • Subject(인터페이스)
    : 관찰되는 대상, 이벤트를 발생시키는 주체
    : Observer를 등록register() or attach()하고 삭제unregister() or deattach()하는 메소드를 가지고 있음
    : Subject에서 모든 Observer에 정보를 전달하는 notify 메소드를 가지고 있음
    : Subject는
    Observer들을 알고 있는 객체_로 여러 Observer가 Subject에 붙을 수 있음

  • ConcreteSubject(Subject1)
    : 구체적으로 관찰되는 대상
    : ConcreteObserver에게 알려줘야 하는 상태를 저장해야 함!
    : 상태가 변화하면 ConcreteObserver들에게 notify()한다. notify하게 되면 루프를 돌면서 각 Observer를 Update한다.

  • Observer(인터페이스)
    : 객체(Subject)로 부터 상태 변화를 전달 받는 역할
    : update()를 통해 상태 변화를 전달받는다.
    → 스스로 감시하다가 변화를 알아차리는 것이 아니라, 변화를 통지받고 변화를 알게 되는 것!

  • ConcreteObserver(Observer1)
    : 구체적인 Observer
    : update()가 호출되면 메소드 안에서 Subject의 현재 상태 취득하고 처리


❗ 느슨한 결합

  • Subject와 Observer가 느슨한 결합을 갖는 것이 중요,
    : Observer 등록 순서 등에 특정 로직이 의존하지 않도록 한다.
  • 아래 헤드퍼스트 예제 참고!

❗옵저버는 상태를 갖지 않아도 된다

  • 상태는 Subject의 담당이고 getState()로 상태를 취득하면 됨!!

❗ Notify를 누가 호출해야 할까?
1. Subject 에서 변경이 발생할 때, 변경을 저장하는 메소드가 Notify()를 호출하는 방법. (ex. 아래 코드에서 postmessage())
2. 사용자(main 등)적절한 시기에 Notify()를 호출하는 방법.

cf) 참고
Observer를 attach 할 때 관심사를 함께 등록하는 방법도 있다. 이 방법을 사용해 구현하면 Observer 마다 다른 정보를 전달해 주도록 할 수 있다.
void Subject::Attach(Observer*, Aspect& interest);

Update 메소드의 인자

  • void update(Subject theChangedSubject)
    → GoF의 예제에 등장하는 Update 메소드로, Subject를 넘겨주고, 옵저버가 넘겨받은 Subject에서 필요한 값을 얻는 방법
  • 반드시 지켜야 하는 약속은 아님
    → 상황에 따라 다른 인자를 함께 넘기는 방법
    void update(Subject theChangedSubject, int changedCount)
    → 단순히 값을 전달
    void update(int value1, int value2)

Observer의 행위가 Subject에 영향을 주는 경우

  • 일반적인 Observer 패턴에서 Subject 역할이 다른 클래스로부터 update 메소드의 호출을 요청받는 경우가 존재
    EX) GUI에서 사용자가 버튼을 눌러 요청하면 update 메소드가 호출되는 경우!
  • Subject가 update 메소드를 호출할 때 Observer가 Subject의 메소드를 호출을 요청하는 경우가 있는데 이 경우 무한 루프가 발생할 수 있음
    Subject의 상태가 변화 → Subject가 notify를 호출 → Observer의 update가 호출 → Observer::update 실행도중 Subject의 메소드 호출 → Subject의 상태가 변화(반복)
  • Observer에 플래그 변수를 하나 추가하여 Observer가 현재 update 중인지 아닌지(subject로 부터 전달받고 있는 중인지 아닌지) 상태를 기록하여 해결
// 기계인간님의 예시 코드 
boolean isUpdate;
@Override
public void update(Subject s) {
    if (!isUpdate) {
        return;
    }
    this.subject = s;
}


장점

  • Subject와 Observer 클래스 사이에는 추상적인 결합도만 존재
  • 브로드캐스트 방식의 교류 가능 : Subject는 변화 발생 시 구체적인 수신자를 지정할 필요가 없다. 상태가 변화했다는 사실만 전달하면 변화를 처리할지 말지는 Observer가 결정

단점

  • Observer 패턴을 구현할 때 많은 프로그래머들이 자주 빠지는 함정은 항상 책의 클래스 다이어그램과 동일한 구조로 구현하려 한다는 것이다.

Observer 패턴을 구현할 때 흔히 생기는 문제점이 두 가지 있다. 그것은 통보 체인과 메모리 누수다. 통보 체인이란, 한 관찰자가 통보를 받았을 때 자신이 다시 그 주체가 되어 또 다른 관찰자에게 통보를 하고, 그 관찰자는 또 다른 관찰자에게 통보하는 식으로 연속적인 통보가 발생하는 것을 말한다. 이런 식의 통보 체인이 불가피한 상황에서 Observer 패턴을 적용하면, 설계가 복잡해지고 디버깅도 어려워진다. 이럴 때는 Mediator 패턴을 도입하면 도움이 된다. 메모리 누수는 관찰자 객체가 제때에 쓰레기 수집garbage collection 되지 않는 현상으로, 통보 주체가 관찰자 객체의 참조를 갖고 있기 때문에 발생한다. 통보 주체가 이것을 제때 삭제해준다면, 메모리 누수를 피할 수 있다.

Observer는 자주 사용되는 패턴이다. 구현하기도 어렵지 않기 때문에, 실제로 필요하지 않을 때에도 이 패턴을 적용하고 싶은 유혹에 빠지기도 한다. 그 유혹을 견뎌내기 바란다. 처음에 통보 기능을 하드 코딩으로 구현했더라도, 나중에 일반화가 필요할 때 언제라도 리팩터링을 통해 Observer 패턴을 구현할 수 있기 때문이다



구현

  • Observable(Subject, 인터페이스나 클래스로 구현)
    ⌞IntegerObservable(구체적인 subject, 상속 or implements)
  • Observer(Observer, 인터페이스나 클래스로 구현)
    ⌞StarObserver(구체적인 Observer, 상속 or implements)
    ⌞CircleObserver(구체적인 Observer)

🤔자바에서 상속 vs 구현?

  • 상속
    : 상속(extends)은 자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용
    : 자식 클래스에서 추가 및 확장을 할 수 있는 것을 말합니다. 이로 인해 재사용성, 중복성의 최소화가 이루어짐
  • 구현
    : 구현(implements)은 부모 인터페이스(interface)를 자식 클래스에서 재정의하여 구현
  • 상속과 구현의 차이
    : 상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현
    : 구현은 상속과는 달리 반드시 부모 클래스의 메서드를 재정의하여 구현


EXAMPLE

JAVA

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

/* Subject 인터페이스 */ 
interface Subject {
    public void register(Observer obj); // 옵저버 등록하는 메소드 
    public void unregister(Observer obj); // 옵저버 삭제하는 메소드 
    public void notifyObservers(); // 옵저버들에게 상태 변화를 알리는 메소드 
    public Object getUpdate(Observer obj);
}

/* Observer 인터페이스 */
interface Observer {
    public void update(); 
}

/* 구체적인 Subject, 주체 */
class Topic implements Subject {
    private List<Observer> observers; // 옵저버들을 알고 있다. 
    private String message; 

	// 생성자 
    public Topic() {
        this.observers = new ArrayList<>();
        this.message = "";
    }
	
    // 옵저버 등록 메소드 
    @Override
    public void register(Observer obj) {
        if (!observers.contains(obj)) observers.add(obj); 
    }

	//옵저버 삭제 메소드 
    @Override
    public void unregister(Observer obj) {
        observers.remove(obj); 
    }

	// 옵저버에게 상태 변화 알림 메소드 -> 각 옵저버들의 update 문을 호출하게 함 
    @Override
    public void notifyObservers() {   
        this.observers.forEach(Observer::update); 
    }

	// Topic(subject)의 업데이트 된 메세지를 반환 
    @Override
    public Object getUpdate(Observer obj) {
        return this.message;
    } 
    
    // 메세지 작성 메소드 여기서 상태변화(msg가 업데이트)가 일어나면 notify 메소드를 호출 
    public void postMessage(String msg) {
        System.out.println("Message sended to Topic: " + msg);
        this.message = msg; 
        notifyObservers();
    }
}

/* 구체적인 Observer */
class TopicSubscriber implements Observer {
    private String name;
    private Subject topic;

	// 생성자
    public TopicSubscriber(String name, Subject topic) {
        this.name = name;
        this.topic = topic;
    }

	// update 함수 
    @Override
    public void update() {
        String msg = (String) topic.getUpdate(this); // 상태 변화(업데이트)된 메세지를 얻음 
        System.out.println(name + ":: got message >> " + msg); // 처리 
    } 
}

/* Demo 클래스 */
public class HelloWorld { 
    public static void main(String[] args) {
        Topic topic = new Topic(); 
        Observer a = new TopicSubscriber("a", topic);
        Observer b = new TopicSubscriber("b", topic);
        Observer c = new TopicSubscriber("c", topic);
        topic.register(a);
        topic.register(b);
        topic.register(c); 
   
        topic.postMessage("amumu is op champion!!"); 
    }
}
/*
Message sended to Topic: amumu is op champion!!
a:: got message >> amumu is op champion!!
b:: got message >> amumu is op champion!!
c:: got message >> amumu is op champion!!
*/ 
  • Topic은 주체이자 객체
  • class Topic implements Subject를 통해 Subject interface를 구현
  • Observer a = new TopicSubscriber("a", topic);으로 옵저버를 선언할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정함!
  • Topic에서 postMessage()로 message를 작성 (message 상태변화 및 업데이트)
    → 옵저버들에게 notifyObservers()로 알림 (옵저버의 update() 호출)
    update()에서 getUpdate()로 topic의 업데이트 된 메세지를 받고 처리

헤드 퍼스트 디자인 패턴의 예제

  • update 메소드의 인자로 Subject가 아니라 각 값을 전달 -> 느슨한 결합!

  1. Observer 인터페이스
public interface Observer {
    public void update(float temp, float humidity, float pressure);
}

  1. Subject 인터페이스
public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers();
}

  1. Subject의 구현체
  • 변경이 발생할 때, Subject에서 nofity()을 호출한다.
import java.util.ArrayList;

public class WeatherData implements Subject {
    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged() {
        notifyObservers();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();  // 변경이 발생할 때, 알림을 돌리는 방법 선택
    }
}

  1. Observer 구현체
  • update 호출시 display가 호출되어 화면이 바뀌게 함
  • 생성자 파라미터로 받은 Subject에 자기 자신을 등록하기 때문에 main 메소드에서 Subject에 옵저버를 일일이 등록하지 않음!
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private int id;
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData, int id) {
        this.id = id;
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temperature = temp;
        this.humidity = humidity;
        display();  // 편의상 여기에 배치
    }

    @Override
    public void display() { // Displayelement 인터페이스 메소드 구현 
        System.out.println("장비 ID: " + id + ", 현재 기온: " + temperature + "도, 습도: " + humidity + "%");
    }
}

  1. Main
public static void main(String[] args) {

    WeatherData weather = new WeatherData();
    CurrentConditionsDisplay current1 = new CurrentConditionsDisplay(weather, 1);
    CurrentConditionsDisplay current2 = new CurrentConditionsDisplay(weather, 2);
    CurrentConditionsDisplay current3 = new CurrentConditionsDisplay(weather, 3);

    weather.setMeasurements(30,65, 30.4f);
    weather.setMeasurements(29,64, 30.5f);
    weather.setMeasurements(30,64, 30.6f);
}

장비 ID: 1, 현재 기온: 30.0도, 습도: 65.0%
장비 ID: 2, 현재 기온: 30.0도, 습도: 65.0%
장비 ID: 3, 현재 기온: 30.0도, 습도: 65.0%
장비 ID: 1, 현재 기온: 29.0도, 습도: 64.0%
장비 ID: 2, 현재 기온: 29.0도, 습도: 64.0%
장비 ID: 3, 현재 기온: 29.0도, 습도: 64.0%
장비 ID: 1, 현재 기온: 30.0도, 습도: 64.0%
장비 ID: 2, 현재 기온: 30.0도, 습도: 64.0%
장비 ID: 3, 현재 기온: 30.0도, 습도: 64.0%

Javascript

자바스크립트에서 프록시 객체를 통해 Observer Pattern을 구현할 수 있다.

function createReactiveObject(target, callback) { 
    const proxy = new Proxy(target, {
        set(obj, prop, value){ // handler set함수 : 속성에 대한 접근을 가로챔. 
            if(value !== obj[prop]){ // 상태 변화를 감지 
                const prev = obj[prop]
                obj[prop] = value // 속성 값 바꿈 
                callback(`${prop}가 [${prev}] >> [${value}] 로 변경되었습니다`)
            }
            return true
        }
    })
    return proxy 
} 
const a = {
    "형규" : "솔로"
} 
const b = createReactiveObject(a, console.log)
b.형규 = "솔로"
b.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다
  • set() 함수를 통해 속성에 대한 접근을 “가로채”형규라는 속성이 솔로에서 커플로 되는 것을 감시
  • 형규(Subject)속성 값(상태 변화)프록시 객체(Observer)감시하고(notify 대신??) 상태변화시 속성을 update하고 "변경되었다고" 처리함!
  • get() : 속성과 함수에 대한 접근을 가로챔
    has() : 함수는 in 연산자의 사용을 가로챔
    set() : 함수는 속성에 대한 접근을 가로챔

🤔프록시(proxy) 객체?

  • 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체
  • 자바스크립트에서 프록시 객체는 두 개의 매개변수를 가짐
    target: 프록시할 대상
    handler: target 동작을 가로채고 어떠한 동작을 할 것인지가 설정이 정해져 있는 함수
  • 구현 코드
const handler = {
    get: function(target, name) { 
        return name === 'name' ? `${target.a} ${target.b}` : target[name]
    }
}
const p = new Proxy({a: 'KUNDOL', b: 'IS AUMUMU ZANGIN'}, handler)
console.log(p.name) // KUNDOL IS AUMUMU ZANGIN

: new Proxy()a와 b 속성을 가지고 있는 객체handler 함수를 매개변수로 넣고 p라는 변수를 선언
: p의 name 속성을 참조(기본적인 동작)하니 a와 b라는 속성밖에 없는 객체가 handler의 “name이라는 속성에 접근할 때 a와 b를 합쳐서 문자열을 만들라” 는 로직에 따라 어떠한 문자열을 만듦
: 즉, 어떠한 속성(name)에 접근할 때 그 부분을 가로채서 어떠한 로직(문자열로 만드는 것)을 강제할 수 있게 함!


Vue.js 3.0

  • 프론트엔드에서 많이 쓰는 프레임워크
  • 프록시 객체를 이용한 옵저버 패턴 : refreactive로 정의하면 해당 값이 변경되었을 때 자동으로 DOM에 있는 값이 변경

    DOM(Document Object Model)
    문서 객체 모델을 말하며, 웹 브라우저상의 화면을 이루고 있는 요소


function createReactiveObject(
    target: Target,
    isReadonly: boolean,
    baseHandlers: ProxyHandler<any>,
    collectionHandlers: ProxyHandler<any>,
    proxyMap: WeakMap<Target, any>
) {
    if (!isObject(target)) {
        if (__DEV__) {
            console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
    }
      
    // target is already a Proxy, return it. target이 이미 Proxy면 바로 return
    // exception: calling readonly() on a reactive object
    if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
    ) {
        return target
    }
      
    // target already has corresponding Proxy, target이 이미 Proxy에 대응하면
    const existingProxy = proxyMap.get(target) // Proxy 객체 얻기
    if (existingProxy) {
        return existingProxy // Proxy 객체 리턴
    }
      
    // only a whitelist of value types can be observed, 값 유형만 감시 가능! 
    const targetType = getTargetType(target) // target의 type 값 얻고
    if (targetType === TargetType.INVALID) { // 유효하지 않으면 그냥 return 
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    ) // Proxy 객체 생성(Observer)
    proxyMap.set(target, proxy) // target(Subject)과 Proxy(Observer) 연결! 
    return proxy
}
  • proxyMap이라는 프록시 객체를 사용했고, 객체 내부의 get(), set() 메서드를 사용


😎 참고자료

면접을 위한 CS 전공지식 노트
Java 언어로 배우는 디자인 패턴 입문
GOF Design Pattern
Observer pattern
옵서버 패턴-위키백과
[Design Pattern] GoF 행위 패턴 - 옵서버 패턴(Observer Pattern)
[디자인패턴] 옵저버 패턴 (Observer Pattern)
옵저버 패턴 - 기계인간

profile
SW Engineer 꿈나무 / 자의식이 있는 컴퓨터

0개의 댓글