옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.
출처 - https://ko.wikipedia.org/wiki/옵서버_패턴
옵저서 패턴은 리스너라고 불리는 하나 이상의 객체에 등록을 시키고 위에 사진에서 보이듯 각각의 옵저버들은 관찰 대상이 발생시키는 이벤트를 받아서 처리한다.
옵저버 패턴이 중요한 이유 중 하나는 객체 간의 느슨한 결합(loose coupling)을 제공하기 때문이다.
느슨한 결합은 시스템의 구성 요소들이 서로 최소한의 의존성을 갖도록 설계하는 것을 의미하고 이는 시스템의 유지보수와 확장성을 높이는데 좋다.
예제 코드
// 주제 (Subject) 클래스
class Subject {
constructor() {
this.observers = []; // 옵저버 목록
}
// 옵저버 등록
addObserver(observer) {
this.observers.push(observer);
}
// 옵저버 제거
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
// 옵저버에게 알림
notifyObservers(message) {
this.observers.forEach(observer => observer.update(message));
}
}
// 옵저버 (Observer) 클래스
class Observer {
constructor(name) {
this.name = name;
}
// 주제로부터 알림을 받는 메서드
update(message) {
console.log(`${this.name} received message: ${message}`);
}
}
// 주제 인스턴스 생성
const subject = new Subject();
// 옵저버 인스턴스 생성
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
// 주제에 옵저버 등록
subject.addObserver(observer1);
subject.addObserver(observer2);
// 주제 상태 변경 및 알림 전송
subject.notifyObservers('Hello Observers!');
// 옵저버 제거
subject.removeObserver(observer1);
// 주제 상태 변경 및 알림 전송 (Observer 1은 알림을 받지 않음)
subject.notifyObservers('Observer 1 removed');
옵저버 패턴의 경우 옵저버와 주체가 서로를 모르는 상태가 아니다.
주체에 옵저버를 등록하는 과정을 통해 서로 알고 있다고 생각할 수 있다.
이제 다음에 설명할 구독-발행 패턴(pub-sub pattern)은 옵저버와 주체가 서로를 전혀 모르는데 간단하게 미리 설명하면
옵저버 - 브로커 - 주체
이런식으로 사이에 중재자를 두는 방식이다.
발행구독 패턴의 주요 구성 요소는 Publisher, Subscriber, Broker이다.
각각의 요소의 특징을 살펴보면 다음과 같다.
옵저버 패턴과 발행 구독 패턴은 서로 매우 유사하다.
그러나 크게 3가지 부분에서 차이점을 보인다.
싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴이다.
만약 인스턴스가 필요하다면, 똑같은 인스턴스를 또 만들지 않고 기존의 인스턴스를 가져와 활용하는 기법이다.
따라서 싱글톤 패턴을 따르는 클래스를 여러번 호출한다고 해도 최초에 생성된 객체를 계속 리턴한다.
파이썬의 모듈은 그 자체로 이미 싱글턴이다.
참고 - https://ko.wikipedia.org/wiki/싱글턴_패턴
그럼 싱글톤 패턴으로 어떻게 만들까? 아래 코드 예시를 보면 확실히 이해가 될 것이다.
예시 코드
class Singleton {
constructor(name) {
if (!Singleton.instance) {
this.subscribers = new Map();
this.name = name;
Singleton.instance = this;
}
return Singleton.instance;
}
static getInstance(name) {
if (!Singleton.instance) {
Singleton.instance = new Singleton(name);
}
return Singleton.instance;
}
getName() {
return this.name;
}
}
// 싱글톤 인스턴스 생성
const instance1 = new Singleton("dannysir의 블로그");
console.log(instance1.getName()); // "dannysir의 블로그"
const instance2 = new Singleton("무언가 바뀐값");
console.log(instance2.getName()); // "dannysir의 블로그"
console.log(instance1 === instance2); // true
모듈 시스템 자체가 싱글톤을 모장하기 때문에 ES6 모듈을 이용하면 더 쉽게 구현이 가능하다.
먼저 singleton.js 파일을 아래와 같이 만든다.
// singleton.js
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = "Singleton Instance";
Singleton.instance = this;
}
getData() {
return this.data;
}
}
const instance = new Singleton();
Object.freeze(instance); // 인스턴스 변경을 방지
export default instance;
위의 코드를 보면 export defualt
를 사용해 인스턴스를 내보내고,
Object.freeze
를 사용해 변경되지 않도록 보호한다.
이제 다른 파일에서 사용하면 다음과 같은 결과가 나온다.
import singletonInstance from './singleton.js';
console.log(singletonInstance.getData()); // "Singleton Instance"
const anotherInstance = singletonInstance;
console.log(singletonInstance === anotherInstance); // true
synchronized
키워드나 더블 체크 락킹(double-checked locking) 방법을 사용할 수 있습니다.페어 프로그래밍(Pair Programming)은 두 명의 개발자가 한 컴퓨터를 공유하여 함께 코드를 작성하는 소프트웨어 개발 기법이다. 이 기법은 애자일 개발 방법론의 한 부분으로, 두 명의 개발자가 협력하여 코드의 품질을 높이고, 버그를 줄이며, 지식을 공유하는 것을 목표로 한다.
페어 프로그래밍의 기본적인 역할은 두가지이며 다음과 같다.
worker threads 모듈은 Node.js에서 멀티스레딩을 통해 싱글 스레드로 돌아가는 javascript에서 분산 처리를 할 수 있게 도와주는 기능을 제공.
백문이 불여일견이다. worker threads 사용 예시를 살펴보며 어떻게 사용하는지 알아보자.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// 메인 스레드에서 워커 스레드를 생성하고 데이터를 전달합니다.
const worker = new Worker(__filename, {
workerData: { value: 42 } // 워커 스레드로 전달할 초기 데이터
});
// 워커 스레드로부터 메시지를 받는 이벤트 핸들러
worker.on('message', (message) => {
console.log('Received from worker:', message); // 워커 스레드로부터 메시지를 받았을 때 처리
});
// 워커 스레드에서 발생한 에러를 처리하는 이벤트 핸들러
worker.on('error', (error) => {
console.error('Worker error:', error); // 워커 스레드에서 에러가 발생했을 때 처리
});
// 워커 스레드가 종료되었을 때 실행되는 이벤트 핸들러
worker.on('exit', (code) => {
if (code !== 0)
console.error(`Worker stopped with exit code ${code}`); // 워커 스레드가 비정상 종료되었을 때 처리
});
// 메인 스레드에서 워커 스레드로 메시지 전송
worker.postMessage('Hello from main thread');
} else {
// 워커 스레드
// 전달된 초기 데이터를 출력
console.log('Worker data:', workerData);
// 메인 스레드로부터 메시지를 받았을 때 실행되는 핸들러
parentPort.on('message', (message) => {
console.log('Received from main thread:', message);
// 메인 스레드로 메시지 전송
parentPort.postMessage(`Worker received: ${message}`);
});
// 메인 스레드로 초기 메시지 전송
parentPort.postMessage(`Worker started with data: ${workerData.value}`);
}
이제 위에서부터 하나씩 뜯어보자.
new Worker(__filename, { workerData: { value: 42 } })
worker.on('message', (message) => { ... })
worker.on('error', (error) => { ... })
worker.on('exit', (code) => { ... })
worker.postMessage('Hello from main thread')
이제 가장 처음에 보여줬던 코드가 이해가 될 것이다.
Worker 클래스
new Worker(filename, [options])
filename
: 워커 스레드로 실행할 JavaScript 파일 경로.options
: 추가 옵션 (e.g., workerData
, stdin
, stdout
).worker.postMessage(value, [transferList])
value
: 워커 스레드로 보낼 메시지.transferList
: ArrayBuffer
객체 등 전송할 수 있는 자원 목록.worker.terminate()
Promise
객체를 반환하며, 종료 시 호출됩니다.worker.ref()
worker.unref()
worker.threadId
worker.resourceLimits
parentPort 객체
parentPort.postMessage(value, [transferList])
value
: 메인 스레드로 보낼 메시지.transferList
: ArrayBuffer
객체 등 전송할 수 있는 자원 목록.parentPort.close()
그럼 이제 terminate()
함수를 이용해 워커 스레드를 강제 종료 시키는 코드를 앞선 코드에 추가 해보자.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename, {
workerData: { value: 42 }
});
worker.on('message', (message) => {
console.log('Received from worker:', message);
});
worker.on('error', (error) => {
console.error('Worker error:', error);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
} else {
console.log('Worker terminated successfully');
}
});
// 메인 스레드에서 워커 스레드로 메시지 전송
worker.postMessage('Hello from main thread');
// 워커 스레드 종료
setTimeout(() => {
worker.terminate().then((code) => {
console.log(`Worker terminated with code ${code}`);
});
}, 5000); // 5초 후 워커 종료
} else {
console.log('Worker data:', workerData);
parentPort.on('message', (message) => {
console.log('Received from main thread:', message);
parentPort.postMessage(`Worker received: ${message}`);
});
parentPort.postMessage(`Worker started with data: ${workerData.value}`);
}
setTimeout()
과 terminate()
함수를 이용해 강제 종료.worker.on('exit', (code) => { ... })
에서 종료를 감지, 콜백 실행.오늘 처음 보든 패턴들에 대해 알게 되었고 각각의 패턴을 어떻게 구현할지 고민했다.
싱글톤 같은 경우에는 이미 알고 있었지만, 이를 활용하는 과정을 통해 단순하게 이론적으로 아는 것을 넘어서 장단점, 주의점 같은 것을 확실히 알게 되었다.
https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber
https://inpa.tistory.com/entry/GOF-💠-싱글톤Singleton-패턴-꼼꼼하게-알아보자#
https://nodejs.org/api/worker_threads.html
https://noodabee.tistory.com/entry/Nodejs-EventEmitter란-feat-Promise와의-차이점
https://80000coding.oopy.io/5fe0df00-c385-413b-899d-759a46868842