본문 바로가기
CS

옵저버 패턴

by Shark_상어 2023. 1. 23.
728x90

옵저버 패턴

옵저버 패턴(observer pattern)은 주체가 어떤 객체(subject)의 상태 변화를 관찰하다가 상태 변화가 있을 때 마다 메서드 등을

통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다.

![img](https://thebook.io/img/080326/035_1.jpg)

여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로

'추가 변화 사항'이 생기는 객체들을 의미한다.

![img](https://thebook.io/img/080326/035_2.jpg)

또한, 위의 그림처럼 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 한다.

옵저버 패턴을 활용한 서비스로는 트위터가 있다.

![img](https://thebook.io/img/080326/036_1.jpg)

위의 그림처럼 내가 어떤 사람인 주체를 '팔로우'했다면 주체가 포스팅을 올리게 되면 알림이 '팔로워'에게 가야한다.

또한, 옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC(Model-View-Controller) 패턴에도 사용된다.

ex) 주체라고 볼 수있는 모델에서 변경사항이 생겨 update() 메서드로 옵저버인 뷰에게 알려주고 이를 기반으로 컨트롤러 등이

작동한다.

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

interface Subject {
    public void register(Observer obj);
    public void unregister(Observer obj);
    public void notifyObservers();
    public Object getUpdate(Observer obj);
}

interface Observer {
    public void update(); 
}

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

    @Override
    public void notifyObservers() {
        this.observers.forEach(Observer::update); 
    }

    @Override
    public Object getUpdate(Observer obj) {
        return this.message;
    } 
    
    public void postMessage(String msg) {
        System.out.println("Message sended to Topic: " + msg);
        this.message = msg; 
        notifyObservers();
    }
}

class TopicSubscriber implements Observer {
    private String name;
    private Subject topic;

    public TopicSubscriber(String name, Subject topic) {
        this.name = name;
        this.topic = topic;
    }

    @Override
    public void update() {
        String msg = (String) topic.getUpdate(this); 
        System.out.println(name + ":: got message >> " + msg); 
    } 
}

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을 기반으로 옵저버 패턴을 구현했습니다. 여기서 topic은 주체이자 객체가 된다.

class Topic implements Subject를 통해 Subject interface를 구현했고 Observer a = new TopicSubscriber("a", topic); 으로

옵저버를 선언 할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정했다.

자바: 상속과 구현

잠시 위의 코드에 나온 implements 등 자바의 상속과 구현의 특징과 차이에 대해 알아보자.

 

상속

상속(extends)은 자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가 및 확장을 할 수 있는 것을 말한다. 이로 인해 재사용성, 중복성의 최소화가 이루어진다.

 

구현

구현(implements)은 부모 인터페이스(interface)를 자식 클래스에서 재정의하여 구현하는 것을 말하며, 상속과는 달리 반드시 부모 클래스의 메서드를 재정의하여 구현해야 한다.

 

상속과 구현의 차이

상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현한다.

자바스크립트에서의 옵저버 패턴

자바스크립트에서의 옵저버 패턴은 프록시 객체를 통해 구현할 수도 있다.

 

프록시 객체

프록시(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에 “name이라는 속성에 접근할 때는 a와 b라는 것을 합쳐서 문자열을 만들어라.”를 구현했다.

이렇게 p라는 변수에 name이라는 속성을 선언하지 않았는데도 p.name으로 name 속성에 접근하려고 할 때,

그 부분을 가로채 문자열을 만들어 반환하는 것을 볼 수 있다.

프록시 객체를 이용한 옵저버 패턴

 

자바스크립트

function createReactiveObject(target, callback) { 
    const proxy = new Proxy(target, {
        set(obj, prop, value) {
            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.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다.

프록시 객체의 get() 함수는 속성과 함수에 대한 접근을 가로채며, has() 함수는 in 연산자의 사용을 가로챈다.

set() 함수는 속성에 대한 접근을 가로챈다.

set() 함수를 통해 속성에 대한 접근을 “가로채”서 형규라는 속성이 솔로에서 커플로 되는 것을 감시할 수 있다.

Vue.js 3.0의 옵저버 패턴

![img](https://thebook.io/img/080326/042.jpg)

프론트엔드에서 많이 쓰는 프레임워크 Vue.js 3.0에서 ref나 reactive로 정의하면 해당 값이 변경되었을 때 자동으로 DOM에 있는 값이 변

경되는데, 이는 앞서 설명한 프록시 객체를 이용한 옵저버 패턴을 이용하여 구현한 것입니다.

 

자바스크립트

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.
    // exception: calling readonly() on a reactive object
    if (
        target[ReactiveFlags.RAW] &&
        !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
    ) {
        return target
    }
    // target already has corresponding Proxy
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
    // only a whitelist of value types can be observed.
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}

앞의 코드는 실제로 Vue.js 3.0의 옵저버 패턴이 담긴 코드입니다.

proxyMap이라는 프록시 객체를 사용했고, 객체 내부의 get(), set() 메서드를 사용한 것을 볼 수 있다.

728x90

'CS' 카테고리의 다른 글

이터레이터 패턴이란 무엇인가?  (0) 2023.01.26
프록시 패턴 과 프록시 서버  (0) 2023.01.24
전략 패턴  (0) 2023.01.21
팩토리 패턴  (0) 2023.01.20
디자인 패턴 - 싱글톤 패턴  (0) 2023.01.18