728x90
728x90

옵저버 패턴(Observer Pattern)

  • 주체가 어떤 객체(Subject)의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴
    • 주체 : 객체의 상태 변화를 보고 있는 관찰자
    • 옵저버 :  전달되는 메서드 등을 기반으로, 객체의 상태 변화에 따라 추가 변화 사항이 생기는 객체들
  • 주체 객체를 따로 두지 않고, 상태가 변경되는 객체를 기반으로 구축하기도 한다.

 

ⓒ https://refactoring.guru/design-patterns/observer

 

  • 옵저버 패턴을 활용한 서비스로는 트위터(Twitter)가 있다.
    • 내가 어떤 사람인 주체를 '팔로우' 했다면 주체가 포스팅을 하면 알림이 '팔로워'에게 가게 된다.

트위터의 옵저버 패턴

 

  • 또한 옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며, MVC(Model-View-Controller) 패턴에도 사용된다.
    • 변경 사항이 생겨 주체라고 볼 수 있는 모델(Model)에서 @update()@ 메서드로 옵저버뷰(View)에게 알려주고, 이를 기반으로 컨트롤러(Controller) 등이 작동한다.

ⓒ https://www.underthehoodlearning.com

 

자바에서의 옵저버 패턴

  • 다음 코드는 @topic@을 기반으로 구현한 옵저버 패턴이다.
    • 여기에서 @topic@은 주체이자 객체가 된다.
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!!
*/

 

  • @class Topic implements Subject@를 통해 @Subject interface@를 구현했고, @Observer a = new TopicSubscriber("a", topic);@으로 옵저버를 선언할 때 해당 이름과 어떠한 토픽의 옵저버가 될 것인지를 정하였다.

 

상속과 구현

  • 자바의 상속구현의 특징과 차이에 대해 알아보자.

 

상속(@extends@)

  • 자식 클래스부모 클래스의 메서드 등을 상속 받아 사용한다.
  • 자식 클래스에서 추가 및 확장을 할 수 있다.
  • 상속을 통해 재사용성, 중복의 최소화가 이루어진다.

 

구현(@implements@)

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

 

상속과 구현의 차이?

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

 

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

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

 

프록시(Proxy) 객체

  • 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체
  • 자바스크립트에서 프록시 객체는 2개의 매개변수를 갖는다.
    • @target@ : 프록시할 대상
    • @handler@ : 프록시 객체의 @target@ 동작을 가로채서 정의할 동작들이 정해져 있는 함수

 

프록시 객체 구현 코드

const handler = {
    get: function(target, name) { 
        return name === 'name' ? `${target.a} ${target.b}` : target[name]
    }
}
const p = new Proxy({a: 'PROXY', b: 'IS A THING'}, handler)
console.log(p.name)   // PROXY IS A THING

 

  • @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의 옵저버 패턴

  • 프론트엔드에서 많이 쓴느 프레임워크인 Vue.js 3.0에서 @ref@나 @reactive@로 정의하면 해당 값이 변경되었을 때 자동으로 DOM에 있는 값이 변경된다.
    • 이것은 프록시 객체를 이용하여 구현한 옵저버 패턴을 이용한 것이다.
 

Vue.js - The Progressive JavaScript Framework | Vue.js

Versatile A rich, incrementally adoptable ecosystem that scales between a library and a full-featured framework.

vuejs.org

 

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.
    // 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()@ 메서드를 사용한 것을 확인할 수 있다.

 

참고 사이트

 

Observer

/ Design Patterns / Behavioral Patterns Observer Also known as: Event-Subscriber, Listener Intent Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object t

refactoring.guru

 

옵서버 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체

ko.wikipedia.org

 

728x90
728x90