[Design Patterns] Observer 패턴

·2024년 2월 1일
0

patterns

목록 보기
6/11
post-thumbnail

Observer 패턴

Observer 패턴에서 특정 객체를 구독할 수 있는데 구독하는 주체를 Observer라고 하고, 구독 가능한 객체를 Observable이라고 한다.
이벤트가 발생할 때마다 Observable은 모든 Observer에게 이벤트를 전파한다.

  • observers : 이벤트가 발생할 때마다 전파할 Observer들의 배열
  • subscribe() : Observer를 Observers 배열에 추가한다.
  • unsubscribe() : Observers 배열에서 Observer를 제거한다.
  • notify() : 등록된 모든 Observer들에게 이벤트를 전파한다.

아래는 ES6 클래스를 사용하여 Observable을 구현한 예제이다.

class Observable {
  constructor() {
    this.observers = []
  }
  
  subscribe(func) {
    this.observers.push(func)
  }
  
  unsubscribe(func) {
    this.observers = this.observers.filter(observer => observer !== func)
  }
  
  notify(data) {
    this.observers.forEach(observer => observer(data))
  }
}

subscribe 메서드를 통해 Observer를 등록하고, 반대로 unsubscribe를 통해 등록 해지할 수 있다.
그리고 notify 메서드를 통해 모든 Observer에게 이벤트를 전파할 수 있다.

구현한 Observable 객체를 이용해 Button 컴포넌트와 Switch 컴포넌트를 가진 기본적인 앱을 만들어보자.

export default function App() {
  return (
    <div className="App">
    	<Button>Click me!</Button>
    	<FormControlLabel control={<Switch />} />
  	</div>
  )
}

앱 내에서 일어나는 사용자 인터렉션을 추적하고 싶다. 사용자가 버튼을 클릭하던 스위치를 토글하던 간에 타임스탬프를 로깅하려고 한다. 또 이벤트 발생 시마다 토스트 알림을 화면에 노출하려고 한다.

사용자가 handleClick 또는 handleToggle 함수를 호출할 때마다 핸들러는 Observable의 notify를 호출한다.
notify 메서드는 등록된 모든 Observer에게 handleClick 또는 handleToggle에서 전달된 데이터를 포함한 이벤트를 전파한다.

먼저 logger함수와 toastify함수를 만들자. 이 함수들은 notify 메서드로부터 data를 받게 된다.

import { ToastContainer, toast } from 'react-toastify'

function logger(data) {
  console.log(`${Date.now()} ${data}`)
}

function toastify(data) {
  toast(data)
}

export default function App() {
  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
      <ToastContainer />
    </div>
  )
}

아직까지 Observable과 logger, toastify 함수가 연결되지 않았기 때문에 알림을 받을 수 없다.
이 함수들이 Observer로써 작동하기 위해서는 Observable의 subscribe 메서드를 사용해야 한다.

import { ToastContainer, toast } from 'react-toastify'

function logger(data) {
  console.log(`${Date.now()} ${data}`)
}

function toastify(data) {
  toast(data)
}

observable.subscribe(logger)
observable.subscribe(toastify)

export default function App() {
  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
      <ToastContainer />
    </div>
  )
}

이제 이벤트가 발생할 때마다 loggertoastify 함수는 알림을 받게 될 것이다. 이제 Observable에 이벤트를 전파하는 코드만 작성하면 된다.
버튼 클릭에 해당하는 handleClick, 스위치 토글에 해당하는 handleToggle 이벤트 핸들러를 만들고 안에서 Observable의 notify를 실행해 이벤트를 전파한다. 이 때 Observer에서 필요한 데이터를 인자로 넘긴다.

import { ToastContainer, toast } from 'react-toastify'

function logger(data) {
  console.log(`${Date.now()} ${data}`)
}

function toastify(data) {
  toast(data)
}

observable.subscribe(logger)
observable.subscribe(toastify)

export default function App() {
  function handleClick() {
    observable.notify('User clicked button!')
  }

  function handleToggle() {
    observable.notify('User toggled switch!')
  }

  return (
    <div className="App">
      <Button>Click me!</Button>
      <FormControlLabel control={<Switch />} />
      <ToastContainer />
    </div>
  )
}

전체 플로우를 살펴보면, handleClickhandleToggle이 Observable의 notify를 호출하고, 뒤이어 이를 구독하고 있던 Observer loggertoastify 함수는 이 이벤트를 받아 특정 동작을 수행한다.

앱 내에서 인터렉션이 발생하는 동안 loggertoastifynotify의 호출로부터 이벤트를 계속 받을 수 있다.

Observer 패턴은 다양하게 활용할 수 있지만 비동기 호출 혹은 이벤트 기반 데이터를 처리할 때 매우 유용하다.
만약 어떤 컴포넌트가 특정 데이터의 다운로드 완료 알림을 받기 원하거나 사용자가 메시지 보드에 새로운 메시지를 게시했을 때 모든 멤버가 알림을 받거나 하는 등의 상황에 유용하다.

활용 사례

RxJS는 Observer 패턴을 구현한 유명 오픈소스 라이브러리이다.
RxJS를 사용하면 Observable과 Observer(Subscriber)를 만들어낼 수 있다.
아래 예제는 공식 문서에 소개된 것으로 사용자가 문서를 드래그 중인지 아닌지 콘솔에 출력해준다.
RxJS는 이것 말고도 Observer 패턴에 대한 많은 빌트인 기능들을 제공한다.

import React from "react";
import ReactDOM from "react-dom";
import { fromEvent, merge } from "rxjs";
import { sample, mapTo } from "rxjs/operators";

import "./styles.css";

merge(
  fromEvent(document, "mousedown").pipe(mapTo(false)),
  fromEvent(document, "mousemove").pipe(mapTo(true))
)
  .pipe(sample(fromEvent(document, "mouseup")))
  .subscribe(isDragging => {
    console.log("Were you dragging?", isDragging);
  });

ReactDOM.render(
  <div className="App">Click or drag anywhere and check the console!</div>,
  document.getElementById("root")
);

장점, 단점

장점

Observer패턴을 사용하는 것 역시 관심사의 분리와 단일 책임의 원칙을 강제하기 위한 좋은 방법이다. Observer 객체는 Observable 객체와 강하게 결합되어 있지 않고 언제든지 분리될 수 있다. Observable 객체는 이벤트 모니터링의 역할을 갖고, Observer는 받은 데이터를 처리하는 역할을 갖게 된다.

단점

Observer가 복잡해지면 모든 Observer들에 알림을 전파하는 데 성능 이슈가 발생할 수 있다.

< 출처 : https://patterns-dev-kr.github.io/design-patterns/observer-pattern/ >

profile
개발을 개발새발 열심히➰🐶

0개의 댓글