2024-07-25 (TIL)

SanE·2024년 7월 25일
0

컴퓨터공학

목록 보기
16/23

👨🏻‍💻학습 내용


💡옵서버 패턴

옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.

출처 - https://ko.wikipedia.org/wiki/옵서버_패턴

옵저서 패턴은 리스너라고 불리는 하나 이상의 객체에 등록을 시키고 위에 사진에서 보이듯 각각의 옵저버들은 관찰 대상이 발생시키는 이벤트를 받아서 처리한다.

  • Observer(관찰자) : 상태 변화를 감지하는 대상. 옵저버에는 함수, 객체 모두 등록 가능.
  • 주체(Subject): 상태를 가지고 있으며, 옵저버를 추가/제거하고 상태 변경 시 옵저버에게 알림을 보냅니다.

옵저버 패턴이 중요한 이유 중 하나는 객체 간의 느슨한 결합(loose coupling)을 제공하기 때문이다.

느슨한 결합은 시스템의 구성 요소들이 서로 최소한의 의존성을 갖도록 설계하는 것을 의미하고 이는 시스템의 유지보수와 확장성을 높이는데 좋다.

느슨한 결합의 이점

  1. 유연성: 주제와 옵저버는 서로 독립적으로 변경 가능. 주체는 옵저버가 어떻게 구현되는지 알 필요가 없으며, 옵저버는 주제가 어떻게 상태를 관리하는지 알 필요가 없다.
  2. 확장성: 새로운 옵저버를 추가해도 주체 코드에 변화가 필요 없다. 주체는 단순히 새로운 옵저버만 등록하면 된다.
  3. 재사용성: 옵저버 패턴을 사용하면 다양한 주체에 동일한 옵저버를 사용할 수 있어 코드의 재사용성이 좋다.
  4. 유지보수성: 느슨하게 결합된 시스템은 각 구성 요소가 독립적이기 때문에 버그 수정이나 기능 추가 시 다른 부분에 미치는 영향이 최소화 된다.

예제 코드

// 주제 (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)은 옵저버와 주체가 서로를 전혀 모르는데 간단하게 미리 설명하면

옵저버 - 브로커 - 주체

이런식으로 사이에 중재자를 두는 방식이다.

💡 Pub-Sub Pattern (발행구독 패턴)


발행구독 패턴의 주요 구성 요소는 Publisher, Subscriber, Broker이다.

각각의 요소의 특징을 살펴보면 다음과 같다.

구성 요소

  • 발행자(Publisher):
    • 메시지를 생성하고 브로커에게 전송.
    • 구독자가 누구인지 알 필요 X.
    • 주로 이벤트 소스 역할.
  • 구독자(Subscriber):
    • 브로커로부터 메시지를 받는다..
    • 특정 주제나 메시지 유형에 관심을 등록.
    • 메시지를 처리하여 필요한 작업을 수행.
  • 브로커(Broker):
    • 발행자로부터 메시지를 받아 구독자에게 분배.
    • 메시지 전달의 중재자 역할.
    • 메시지 큐, 이벤트 버스 등으로 구현 가능.
    • 메시지 필터링 및 라우팅 기능을 제공.

옵저버 패턴과 차이점

옵저버 패턴과 발행 구독 패턴은 서로 매우 유사하다.

그러나 크게 3가지 부분에서 차이점을 보인다.

  1. 구조 및 관계:
    • 옵저버 패턴: 주체(Subject)와 옵저버(Observer) 간의 일대다 관계로 직접적으로 통신합니다. 주체는 옵저버 목록을 관리하고 상태 변화 시 모든 옵저버에게 알립니다.
    • 발행-구독 패턴: 발행자(Publisher)와 구독자(Subscriber) 간의 관계를 브로커(Broker)가 중재합니다. 발행자는 브로커에게 메시지를 보내고, 브로커는 구독자에게 메시지를 전달합니다.
  2. 의존성:
    • 옵저버 패턴: 주체와 옵저버 간에 상호 의존성이 있습니다. 주체는 옵저버를 알고 있어야 하며, 옵저버도 주체를 참조합니다.
    • 발행-구독 패턴: 발행자와 구독자는 서로 독립적입니다. 브로커를 통해 간접적으로 통신하므로, 발행자는 구독자의 존재를 알 필요가 없습니다.
  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
  • 클래스의 생성자가 호출될 때 이미 인스턴스가 존재하면, 기존의 인스턴스를 리턴.
  • 클래스에서 instance라는 변수를 통해 관리.

ES6 모듈을 사용한 싱글톤 패턴

모듈 시스템 자체가 싱글톤을 모장하기 때문에 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

장점

  1. 인스턴스의 유일성 보장: 시스템 전역에서 유일한 인스턴스를 공유하여 자원을 효율적으로 사용.
  2. 글로벌 접근성: 애플리케이션의 어느 부분에서든 인스턴스에 접근할 수 있어 편리함.
  3. 제어된 인스턴스 생성: 인스턴스 생성을 한 곳에서 관리하여 객체의 생명 주기가 명확함.

단점

  1. 테스트 어려움: 단위 테스트를 진행 할 때, 테스트가 서로 독립적이어야 하지만, 싱글톤은 자원을 공유하기 때문에 값이 바뀌게 된다. 따라서 매번 테스트를 진행할 때마다 초기화를 해야 한다.
  2. 모듈간의 의존성이 높다: 싱글톤의 경우 객체를 미리 생성하고 정적 메소드를 이용하기 때문에 클래스 사이에 강한 의존성과 높은 결합이 생긴다. 쉽게 말해 하나의 싱글톤 클래스를 여러 모듈이 공유해서 이를 참조하는 모듈은 인스턴스가 변경될 때마다 변경해야 한다.
  3. 병렬 처리 문제: 멀티스레드 환경에서 동기화 문제를 고려해야 합니다. 이를 해결하기 위해 synchronized키워드나 더블 체크 락킹(double-checked locking) 방법을 사용할 수 있습니다.

💡페어 프로그래밍


페어 프로그래밍(Pair Programming)은 두 명의 개발자가 한 컴퓨터를 공유하여 함께 코드를 작성하는 소프트웨어 개발 기법이다. 이 기법은 애자일 개발 방법론의 한 부분으로, 두 명의 개발자가 협력하여 코드의 품질을 높이고, 버그를 줄이며, 지식을 공유하는 것을 목표로 한다.

페어 프로그래밍의 기본적인 역할은 두가지이며 다음과 같다.

  1. 드라이버(Driver): 코드 작성 작업을 직접 수행하는 사람. 드라이버는 키보드를 사용해 코드를 입력하고, 실시간으로 문제를 해결하며, 구현 세부 사항을 처리.
  2. 내비게이터(Navigator): 코드 작성 과정을 지켜보며 전략적이고 전반적인 시각에서 도움을 주는 사람. 내비게이터는 코드를 검토하고, 다음 단계의 계획을 세우며, 문제를 제기하거나 해결책을 제안.

장점

  • 즉각적인 피드백: 코드 작성 중에 오류나 개선점이 즉시 발견되므로 버그가 조기에 수정될 수 있다.
  • 지식 공유: 개발자들 간의 지식과 경험이 공유되어 팀 전체의 기술 수준이 향상된다.
  • 코드 품질 향상: 두 사람의 협력으로 더 높은 품질의 코드가 작성될 가능성이 높아진다.
  • 문제 해결 능력 향상: 두 명의 시각에서 문제를 바라보므로 더 창의적이고 효과적인 해결책을 찾을 수 있다.

단점

  • 체력 소모 : 지속적으로 말을 주고 받아야 하고 코드를 작성해야 하기 때문에 체력 소모가 심하다. 따라서 주기적으로 휴식을 취해야 한다.
  • 생산성 저하 : 실력이 비슷하다면 시너지 효과가 나지만, 차이가 많이 나면 한 쪽이 설명하는데 너무 많은 시간을 쓰게 된다.
  • 감정 문제 : 서로 맞지 않는다면, 개발 스타일이나 의사소통 방식 때문에 힘들어진다.

종류

  • 핑퐁 프로그래밍 : TDD와 결합한 기법
    1. A가 테스트 케이스 작성, 키보드를 B에게 넘김.
    2. B가 통과 하는 코드 작성.
    3. B가 실패하는 새로운 테스트 케이스 작성, 키보드를 A에게 넘김
    4. A가 테스트 통과
    5. (위 과정 반복)
  • pomodoro 기법 : 일과 휴식 시간을 짧게 반복하여 시간당 효율을 올리는 기법.
    1. 다음에 진행할 작업 결정.
    2. 드라이버, 네비게이터 역할 나누기.
    3. 타이머를 적정한 시간으로 설정 (전통적인 경우 25분)
    4. 타이머가 울리면 작업 중지.
    5. 짧은 휴식 (5~10분), 역할 교대
    6. 위 과정을 3 ~ 4회 반복후 긴 휴식 (15~30분)

💡worker threads ?

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 } })
    • 현재 파일을 워커 스레드로 생성하고, workerData를 통해 초기 데이터를 전달.

이벤트 핸들러 등록

  • worker.on('message', (message) => { ... })
    • 워커 스레드로부터 메시지를 받았을 때 호출되는 콜백 함수를 등록.
  • worker.on('error', (error) => { ... })
    • 워커 스레드에서 발생한 에러를 처리하는 콜백 함수를 등록.
  • worker.on('exit', (code) => { ... })
    • 워커 스레드가 종료되었을 때 호출되는 콜백 함수를 등록.

워커 스레드로 메시지 전송

  • worker.postMessage('Hello from main thread')
    • 워커 스레드로 메시지를 보냅니다.

로직 해석

이제 가장 처음에 보여줬던 코드가 이해가 될 것이다.

  • 메인 스레드.
    • 워커 생성.
      • 데이터 전달.
    • 이벤트 등록.
    • 워커에 메시지 전송
  • 워커 스레드.
    • 받은 데이터 출력.
    • 이벤트 등록.
    • 메인 스레드로 메시지 전송.

주요 메서드

Worker 클래스

  1. new Worker(filename, [options])

    • filename: 워커 스레드로 실행할 JavaScript 파일 경로.
    • options: 추가 옵션 (e.g., workerData, stdin, stdout).
  2. worker.postMessage(value, [transferList])

    • value: 워커 스레드로 보낼 메시지.
    • transferList: ArrayBuffer 객체 등 전송할 수 있는 자원 목록.
  3. worker.terminate()

    • 워커 스레드를 강제로 종료합니다.
    • Promise 객체를 반환하며, 종료 시 호출됩니다.
  4. worker.ref()

    • 워커의 생명주기를 이벤트 루프의 다른 이벤트에 연결합니다. (기본값)
  5. worker.unref()

    • 워커가 완료되지 않아도 프로그램이 종료될 수 있게 합니다.
  6. worker.threadId

    • 워커의 고유 ID를 반환합니다.
  7. worker.resourceLimits

    • 워커 스레드의 메모리 한계를 설정하거나 반환합니다.

parentPort 객체

  1. parentPort.postMessage(value, [transferList])

    • value: 메인 스레드로 보낼 메시지.
    • transferList: ArrayBuffer 객체 등 전송할 수 있는 자원 목록.
  2. 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}`);
}
  1. setTimeout()terminate()함수를 이용해 강제 종료.
  2. worker.on('exit', (code) => { ... }) 에서 종료를 감지, 콜백 실행.

🧐일일 회고

오늘 처음 보든 패턴들에 대해 알게 되었고 각각의 패턴을 어떻게 구현할지 고민했다.

싱글톤 같은 경우에는 이미 알고 있었지만, 이를 활용하는 과정을 통해 단순하게 이론적으로 아는 것을 넘어서 장단점, 주의점 같은 것을 확실히 알게 되었다.

참고 자료

https://gobae.tistory.com/122

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://noodabee.tistory.com/entry/Nodejs-EventEmitter%EB%9E%80-feat-Promise%EC%99%80%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90

https://80000coding.oopy.io/5fe0df00-c385-413b-899d-759a46868842

https://ko.wikipedia.org/wiki/옵서버_패턴

profile
완벽을 찾는 프론트엔드 개발자

0개의 댓글

관련 채용 정보