Observer 패턴이란?
객체 간의 일대다(1:N) 관계에서 한 객체의 상태가 변화할 때, 그 객체에 의존하는 다른 객체들이 알림을 받아 변화를 반영할 수 있게 하는 패턴
구독한 객체에게 이벤트가 발생할 때마다 관찰자가 알림을 받을 수 있다.
- Subject / Observable (주체) : 상태를 가지고 있으며, 관찰자에게 상태 변화를 통지하는 객체
- 관찰자 등록/제거 메서드 가짐 - subscribe(), unsubscribe()
- 상태가 변경되면 모든 관찰자에게 변화를 알림 - notify()
- Observer (관찰자) : 주체의 상태 변화를 수신하여 처리하는 객체
- 관찰 대상이 업데이트 되면 그에 반응하는 메서드 정의
// Subject 객체 정의 class Subject { 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)); } }
Subject
객체를 정의했다. 관찰자들의 배열을 가지고, 관찰자 등록/제거/알림전송 3가지 메서드를 가진다.export default function App(){ return ( <div className="App"> <Button>Click me!</Button> <FormControlLabel control={<Switch />} /> </div> ) }
Button
, Switch
컴포넌트를 가진 App을 만들었다. 사용자가 버튼을 클릭하거나 스위치를 토글할 때마다 타임스탬프를 기록하고 토스트 알림을 화면에 노출할 것이다.import {ToastContainer, toast} from 'react-toastify' function logger(data){ // 타임스탬프 기록 console.log(`${Date.now()} ${data}`); } function toastify(data){ // 토스트 알림 toast(data); } const subject = new Subject(); // subject 인스턴스 생성 subject.subscribe(logger); // logger 함수를 관찰자로 등록 subject.subscribe(toastify); // toastify 함수를 관찰자로 등록 export default function App(){ return ( <div className="App"> <Button>Click me!</Button> <FormControlLabel control={<Switch />} /> <ToastContainer /> </div> ) }
Subject의
notify()
로부터 data를 받게 될 logger
, toastify
함수를 만들었다. 이 두 함수가 observer로써 작동하기 위해 subject
인스턴스를 생성하고 subscribe()
를 사용했다. 이제 이벤트가 발생할 때마다 두 함수는 토스트 알림을 받게된다.export default function App(){ function handleClick(){ subject.notify("User cliked button!"); } function handleToggle(){ subject.notify("User toggled switch!"); } return ( <div className="App"> <Button variant="contained" onClick={handleClick}> Click me! </Button> <FormControlLabel control={<Switch onChange={handleToggle} />} label="Toggle me!" /> <ToastContainer /> </div> ); }
버튼 클릭
handleClick
, 스위치 토글 handleToggle
이벤트 핸들러를 만들고 그 안에서 nofify()
를 실행한다. 이렇게 이벤트가 발생하면 Subject의 notify
는 observer인 logger
, toastify
함수에게 data를 인자로 넘긴다.전체 플로우는 아래의 영상과 같다.
위 예제의 실행 결과는 다음과 같다.
활용예제
뉴스 구독 시스템
NewsSubject
: Subject 역할. 뉴스 주제
NewsSubscriber
: Observer 역할. 구독자
Observer가 함수가 아닌 사람인 경우! 더 직관적
// Subject 클래스 정의 class NewsSubject { constructor() { this.observers = []; // 구독자 목록 } // 구독자 추가 subscribe(observer) { this.observers.push(observer); } // 구독자 제거 unsubscribe(observer) { this.observers = this.observers.filter(sub => sub !== observer); } // 모든 구독자에게 알림 전송 notify(news) { this.observers.forEach(observer => observer.update(news)); } } // Observer 클래스 정의 class NewsSubscriber { constructor(name) { this.name = name; } // 뉴스 업데이트 시 호출될 메서드 update(news) { console.log(`${this.name} received news: ${news}`); } } // 주제 인스턴스 생성 const newsSubject = new NewsSubject(); // 구독자 인스턴스 생성 const subscriber1 = new NewsSubscriber("Subscriber 1"); const subscriber2 = new NewsSubscriber("Subscriber 2"); // 구독자 등록 newsSubject.subscribe(subscriber1); newsSubject.subscribe(subscriber2); // 뉴스 업데이트 (구독자들에게 알림 전송) newsSubject.notify("Breaking News: Observer Pattern in Action!"); // 구독자 제거 후 알림 테스트 newsSubject.unsubscribe(subscriber1); newsSubject.notify("Another Update: Only Subscriber 2 receives this.");
예제 실행결과
Subscriber 1 received news: Breaking News: Observer Pattern in Action!
Subscriber 2 received news: Breaking News: Observer Pattern in Action!
Subscriber 2 received news: Another Update: Only Subscriber 2 receives this.
사례분석
RxJS : Observer 패턴을 구현한 유명 오픈소스 라이브러리
Reactive X는 Observer 패턴, 이터레이터 패턴, 함수형 프로그래밍을 조합하여 이벤트의 순서를 이상적으로 관리할 수 있다.
RxJS 를 사용하면 Subject를 따로 정의하지 않아도 Subject, 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( // mousedown : 마우스 클릭 이벤트 fromEvent(document, "mousedown").pipe(mapTo(false)), // mousemove : 마우스 드래그 이벤트 fromEvent(document, "mousemove").pipe(mapTo(true)) ) // mouseup : 마우스 버튼을 누르고 있다가 떼는 순간 이벤트 .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") );
장점
- 느슨한 결합 (Loose Coupling) : 주체와 관찰자가 직접적으로 서로를 참조하지 않고 순수하게 상태 변경에만 집중하면 된다. ⇒ 결합도가 낮아지고 유지보수가 쉬워진다
- 확장성 : 새로운 관찰자 추가 또는 제거가 간단하다.
- 실시간 업데이트 : 주체의 상태가 변경되어 실시간 데이터 업데이트가 필요한 경우에 유용하게 활용된다.
- 재사용성 : 주체와 관찰자가 각각 독립적으로 재사용 될 수 있다.
단점
- 성능 저하 : 관찰자가 많아지면 성능이 저하될 수 있다.