회고

오늘은 Sync Cafe 주제로 미션이 주어졌다. 바리스타를 하나의 Tread로 보고 주문을 동기적으로 하나씩 커피를 만드는 프로그램을 개발하는 것이었다. 멀티 스레드 방식으로 구현하기 위해, 브라우저에서 지원하는 Web Worker을 이용하여 Tread Pool를 구성하고, 각 스레드(바리스타)가 주문서(Queue)에서 하나씩 pop()하여 커피를 마시도록 구현을 하였다. 오늘 학습한 내용은 두 가지이다.

Web Worker

웹 워커는 백그라운드 스레드에서 스크립트를 실행할 수 있는 브라우저에서 지원하는 기능이다. 워커 스레드는 자식 워커를 생성할 수 있고, 부모/자식 스레드 간에는 이벤트 핸들러를 이용하여 메시지를 주고 받을 수 있다. 만약 Web Application을 구현한다면, 특정 기능을 Polling 방식으로 업데이트되어야 할 경우에 사용하면 유용할 것으로 보인다.

워커는 생성시 새로운 Global Context에서 실행된다. 즉, 기존의 Window과 독립된 Global Context로 실행된다는 것이다. 따라서, 워커에서 현재 Window에 접근하게 되면 에러가 발생한다. 워커는 dedicated workers, shared worker 두 가지로 분류되며, 각각 DedicatedWorkerGlobalScope, SharedWorkerGlobalScope의 글로벌 스코프를 가진다.

워커 내부에서는 DOM 객체를 직접 제어할 수 없고, Window 객체의 몇 가지 Property를 사용에 제한이 된다. 하지만, WebSockets 및 IndexedDB 등의 API는 사용이 가능하다.

Dedicated Worker는 스크립트에서 바로 생성할 수 있고, 스레드와 onmessage 이벤트 핸들러와 postMessage 메서드로 부모/자식 간에 메시지 전달이 가능하다. 결과적으로 자신을 호출한 부모 스레드에만 접근이 가능하다.

// 브라우저에서 Web Worker 지원 여부
if (window.Worker) {

  // 생성
  const worker = new Worker("worker.js");    

  // 자식 스레드로 부터 메시지를 전달받는 이벤트 핸들러
  worker.onmessage = function(e) {
    // ... 메시지 처리 로직
  }

  // 부모 스레드로 메시지 전달
  worker.postMessage(message);
  }

}

Tread Pool

Thread Pool은 Thread 생성 비용을 줄이고자 제안된 디자인 패턴이다. Pool에 Thread를 미리 생성하여 가지고 있다. Pool은 동기화된 Queue로 볼 수 있으며, 요청에 따라 Thread를 dequeue하고 작업이 끝난 Thread는 enqueue한다.

Pool의 Thread 개수는 대기중인 작업 수에 따라 응용 프로그램 수명 동안 동적으로 조정이 가능하다. 만약 Tread 요청이 많다면 스레드를 추가로 생성할 수 있고, 요청이 줄어든다면 일정 스레드만 남기고 제거할 수 있다. 스레드 개수를 조정하는 알고리즘은 전체 성능에 영향을 미치는 중요한 요소이다.

또한, 몇 가지 유의해야 할 점이 있다. 필요한 스레드보다 많이 생성해 놓으면 메모리 등의 리소스가 낭비되고, 너무 많이 삭제하면 기존과 동일하게 생성 비용이 추가된다.

Tread Pool

설계 방식

오늘 미션에 대한 처음 설계는 Coffee, Barista, Order, Manager, Cafe 객체를 구성하였다. 해당 객체를 기준으로 책임 주도 방식으로 설계를 시도하였다.

Coffee

  • 커피 객체는 "커피 생산"의 책임을 갖는다.
  • 커피 객체는 Barista로 부터 "생산" 메시지를 받으면, 일정 시간동안 Delay 한다.
  • 커피는 아메리카노, 라떼, 프라프치노 세 가지가 존재하고, 각각 Delay Time이 다르다.

Barista

  • 바리스타 객체는 "커피 주문 처리"의 책임을 갖는다.
  • 매니저로 부터 받은 주문서를 기준으로 해당하는 커피 객체를 찾고 "생산" 메시지를 요청한다.

Manager

  • 매니저 객체는 "바리스타에게 주문서을 주는" 책임을 담당한다.
  • 주문목록(Order) 객체에게 주문서를 요청한다.
  • 주문서가 존재하면 Tread Pool에서 쉬고 있는 바리스타 객체에게 "커피 주문 처리" 메시지를 전달한다.

Order

  • 주문목록 객체는 "주문서를 저장"하는 책임을 갖는다.
  • Order(주문목록) 객체는 매니저 객체로 부터 "주문서 요청" 메시지를 받으면, 오래된 주문서(FIFO)를 매니저에게 전달한다.

Cafe

  • 카페 객체는 "고객(Client)에게 주문받는" 책임을 갖는다.
  • 고객에게 입력받은 주문목록을 Order 객체에게 저장하도록 요청한다.

IMG_2774.JPG

단일 책임 원칙을 지키며 책임의 단위를 최소화하고자 설계하였다. 하지만, 실제 구현 과정에서는 몇 가지 아쉬운 점이 있었다.

  • Web Worker를 처음 사용해보며, 객체간 메시지 요청/전달 방법이 까다로웠다.

  • Coffee 객체의 책임이 Delay 밖에 없어, Barista 객체에서 바로 처리하도록 구현하였다.
    => 만약 커피 종류가 많아지면, 복잡도가 높아질 수 있기 때문에 Coffee 객체를 구성하는 것이 기능 확장성 부분에서 좋다고 생각한다.

  • Order 객체도 책임이 저장하는 것밖에 없다고 생각하여 Manager 객체에서 동적 배열로 관리하였다.
    => 저장하는 구조를 Tree 등으로 변경하거나, 처리해야 할 주문을 고르는 알고리즘을 추가할 경우를 고려하여 Order 객체로 분리하는 것이 확장성 부분에서 좋았을 것으로 보인다.

  • Thread Pool을 동적 배열로 관리하였는데 제대로 이용하지 못하였다.
    => 주문목록에서 주문서를 꺼내오고, 이를 Barista에게 전달하는 함수를 구현해 놓았는데 재사용하지 못하였다.
    => 재귀적으로 Barista가 주문목록을 확인하고 재호출하는 방식이었는데, 동일한 기능을 여러 군데서 구현하며 응집력이 떨어졌다.
    => 이렇게 구현하면, 새로운 요구사항으로 기능 변경 과정에서 버그를 노출할 가능성이 커진다.

참고