티스토리 뷰


이 글은 4 JavaScript Design Patterns You Should Know를 번역한 글입니다.

모든 개발자는 유지 보수 가능하고 읽기 쉽고 재사용 가능한 코드를 작성하려고 노력합니다. 응용 프로그램이 커질수록 코드 구조화가 중요 해집니다. 디자인 패턴은 이러한 문제를 해결하는 데 중요한 역할을하며 공통된 환경에 대한 조직 구조를 제공합니다.

JavaScript 웹 개발자는 응용 프로그램을 만들 때 무의식적으로도 디자인 패턴과 자주 상호 작용합니다.

비록 특정한 상황에서 사용되는 다양한 디자인 패턴들도 있지만, 일반적으로 JavaScript 개발자들이 많이 쓰는 몇몇의 패턴들이 있습니다.

이번 포스트에서 프로그래밍 스킬을 향상시키기 위해 이러한 일반적인 패턴들에 대해 이야기하고, JavaScript 내부를 깊이 파고들 것입니다.

문제의 디자인 패턴은 다음과 같습니다.

  • Module
  • Prototype
  • Observer
  • Singleton

각 패턴은 살펴볼 것이 많지만, 그 중 다음 주요 사항을 강조 할 것입니다.

  1. Context: 어떤 상황에서 패턴이 사용되었는가?
  2. Problem: 무엇을 해결하려는가?
  3. Solution: 이 패턴을 사용하면 제안된 문제를 어떻게 해결할 수 있는가?
  4. Implementation: 코드 구현은 어떻게 생겼는가?

Module 패턴

JavaScript에서의 모듈은 특정 구성요소를 다른 구성요소와 독립적으로 유지하는데 가장 널리 사용되는 디자인 패턴입니다. 이는 잘 구조화된 코드를 지원하기 위해 느슨한 결합을 제공합니다.

객체지향 언어에 익숙한 사람들을 위해 설명하자면, 모듈은 JavaScript의 “클래스”입니다. 클래스의 많은 장점 중 하나는 캡슐화입니다. 상태 및 동작을 다른 클래스에서 액세스하지 못하도록 보호합니다. 모듈 패턴은 public 및 private 접근 권한 설정을 가능하게 합니다.

모듈은 변수와 메서드를 보호하는 클로저(하지만 함수 대신 객체를 반환해야 합니다) 처럼 private 범위를 허용하는 IIFE (Immediately-Invoked-Function-Expressions)이어야합니다. 다음과 같은 형태입니다.

(function() {

    // private 변수들과 함수들을 선언

    return {
        // public 변수들과 함수들을 선언
    }

})();

클로저 외부에있는 코드는 동일한 범위에 있지 않으므로 private 변수에 접근할 수 없습니다. 보다 구체적인 구현해보겠습니다.

var HTMLChanger = (function() {
  var contents = 'contents'

  var changeHTML = function() {
    var element = document.getElementById('attribute-to-change');
    element.innerHTML = contents;
  }

  return {
    callChangeHTML: function() {
      changeHTML();
      console.log(contents);
    }
  };

})();

HTMLChanger.callChangeHTML();       // Outputs: 'contents'
console.log(HTMLChanger.contents);  // undefined

callChangeHTML은 반환된 객체에 바인딩되며 HTMLChanger 네임스페이스 내부를 참조할 수 있습니다. 그러나 모듈 외부에서는 contents를 참조할 수 없습니다.

REVEALING MODULE 패턴

Revealing Module Pattern 라고 불리는 모듈 패턴의 한 변형이 있습니다. 이 패턴의 목적은 캡슐화를 유지하고 객체 리터럴에서 반환된 특정 변수와 메서드들을 잘 나타내기 위한 것입니다. 다음과 같은 형태입니다.

var Exposer = (function() {
  var privateVariable = 10;

  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }

  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }

  var otherMethodIWantToExpose = function() {
    privateMethod();
  }

  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();

Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined

훨씬 깨끗해 보이지만 private 메서드를 참조할 수 없다는 명백한 단점을 가지고 있습니다. 이는 단위 테스트 문제를 야기할 수 있고, 비슷한 문제로 모듈 외부에서 반환된 메서드들을 오버라이드를 할 수 없게 됩니다.

Prototype 패턴

대부분의 JavaScript 개발자들은 prototype 키워드를 본적이 있거나 프로토타입 상속에 대해 혼란스러워했거나, 프로토타입을 코드에 사용해본 경험이 있을 것입니다. prototype 디자인 패턴은 JavaScript의 프로토타입 상속에 기반하여 만들어졌습니다. prototype 모델은 주로 성능이 중요한 상황에서 객체를 생성하는데 사용됩니다.

생성된 객체는 전달된 원본 객체의 복제본(얕은 복제본)입니다. prototype 패턴의 한 활용사례는 광범위한 데이터베이스 작업을 수행하여 애플리케이션의 다른 부분에 사용되는 객체를 만드는 것이다. 다른 프로세스가 이 객체를 사용해야 할 경우 데이터베이스 작업을 수행하는 대신 이전에 만든 객체를 복제하는 것이 좋습니다.

Prototype Design Pattern on Wikipedia

위 UML은 prototype 인터페이스를 사용하여 완전한 구현체들을 복제하는 방법을 설명합니다.

객체를 복제하려면 생성자가 있어야 첫 번째 객체를 인스턴스화 할 수 있습니다. 그런 다음 prototype 키워드를 이용하여 변수와 메소드를 객체의 구조체에 바인딩합니다. 기본 예제를 살펴 보겠습니다.

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype.go = function() {
  // Rotate wheels
}

TeslaModelS.prototype.stop = function() {
  // Apply brake pads
}

생성자를 사용하면 단일 TeslaModelS 객체를 만들 수 있습니다. TeslaModelS 객체를 만들면 생성자에서 초기화된 상태가 유지됩니다. 또한 prototype 을 사용하여 선언한 gostop 함수를 쉽게 유지보수할 수 있습니다. 위에 살펴본 prototype을 통해 함수를 확장하는 방법과 유사한 방법은 아래와 같습니다.

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype = {
  go: function() {
    // Rotate wheels
  },
  stop: function() {
    // Apply brake pads
  }
}

REVEALING PROTOTYPE 패턴

Prototype 패턴은 Module 패턴과 유사하게 revealing 변형을 가지고 있습니다. Revealing Prototype 패턴은 객체 리터럴을 반환하여 public 및 private 멤버를 캡슐화합니다.

객체를 반환하기 때문에 prototype 객체에 function 키워드를 선언할 것 입니다. 위에서 살펴본 예제를 확장하여 현재의 prototype 객체 안에서 접근권한을 제공할 수 있습니다.

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype = function() {

  var go = function() {
    // Rotate wheels
  };

  var stop = function() {
    // Apply brake pads
  };

  return {
    pressBrakePedal: stop,
    pressGasPedal: go
  }

}();

go와 stop 함수는 반환되는 객체의 밖에 존재하기 때문에 외부로부터 보호되는 것을 확인할 수 있습니다. JavaScript에서 prototype 상속을 지원하기 때문에 기본 기능들을 다시 작성할 필요가 없습니다.

Observer 패턴

어플리케이션의 한 부분이 변경되었을 때 다른 부분들도 같이 변경되어야 하는 경우가 많습니다. AngularJS에서 $scope 객체가 변경되었을 때, 이를 다른 구성요소에 알리기 위해 이벤트를 트리거 할 수 있습니다. Observer 패턴은 이를 구체화 합니다. 객체가 수정되면 종속 객체에 변경 사항을 브로드 캐스팅 합니다.

또 다른 대표젹인 예가 MVC (Model-View-Controller) 아키텍처입니다. Model이 변경되면 View가 업데이트됩니다. 이 아키텍처의 이점 중 하나는 종속성을 줄이기 위해 뷰를 모델에서 분리하는 것입니다.

Observer Design Pattern on Wikipedia

UML 다이어그램에서 볼 수 있듯이 필요한 객체는 subject, observer 그리고 concrete 객체들입니다. Observer 객체는 concrete observer가 notify 메서드를 구현할 수 있도록 하는 추상 클래스입니다.

이벤트 관리를 이용한 observer 패턴을 AngularJS 예제로 살펴보겠습니다.

// Controller 1
$scope.$on('nameChanged', function(event, args) {
    $scope.name = args.name;
});

...

// Controller 2
$scope.userNameChanged = function(name) {
    $scope.$emit('nameChanged', {name: name});
};

Observer 패턴에서 독립적인 객체와 subject 를 구분하는 것이 중요합니다.

Observer 패턴은 많은 이점들을 제공하지만 observer 수가 증가함에 따라 성능이 크게 저하된다는 단점이 있습니다. 가장 유명한 observer 중 하나는 watcher 입니다. AngularJS에서 변수, 함수 그리고 객체들을 관찰(watch)할 수 있습니다. $$digest 주기가 실행되고 관찰중인 객체가 변경될 때마다 새 값으로 각 관찰자에게 알립니다.

JavaScript에서 자체 Subject와 Observer를 만들 수 있습니다. 이것이 어떻게 구현되는지 살펴봅시다.

var Subject = function() {
  this.observers = [];

  return {
    subscribeObserver: function(observer) {
      this.observers.push(observer);
    },
    unsubscribeObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers.splice(index, 1);
      }
    },
    notifyObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers[index].notify(index);
      }
    },
    notifyAllObservers: function() {
      for(var i = 0; i < this.observers.length; i++){
        this.observers[i].notify(i);
      };
    }
  };
};

var Observer = function() {
  return {
    notify: function(index) {
      console.log("Observer " + index + " is notified!");
    }
  }
}

var subject = new Subject();

var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();

subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);

subject.notifyObserver(observer2); // Observer 2 is notified!

subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

PUBLISH/SUBSCRIBE

하지만 Publish/Subscribe 패턴은 알림을 수신하려는 객체(subscribers)와 이벤트를 발생시키는 객체(publisher) 사이에 위치하는 topic/event 채널을 사용합니다. 이 이벤트 시스템을 사용하면 구독자(subscriber)가 필요로하는 값을 사용자 정의 인자로 넘길 수 있는 어플리케이션별 이벤트를 정의할 수 있습니다. 이 방식은 구독자와 게시자 간의 종속성을 피할 수 있습니다.

이는 구독자(subscriber)가 적절한 이벤트 핸들러를 구현하여 게시자(publisher)가 브로드 캐스트하는 topic 알림을 등록하고 수신하므로 Observer 패턴과는 다릅니다.

많은 개발자들이 publish/subscribe 패턴과 observer 패턴이 다르지만 같이 사용합니다. publish/subscribe 패턴의 구독자(subscriber)는 일부 메시징 매체를 통해 알림을 받지만 observer 패턴에서는 subject와 유사한 핸들러를 구현하여 알림을 받습니다.

AngularJS에서 구독자(subscriber)는 $on('event', callback)을 통해 이벤트를 구독하고, 발행자(publisher)는 $emit('event', args) 혹은 $broadcast('event', args)를 통해 이벤트를 발행합니다.

Singleton 패턴

Singleton 패턴은 단일 인스턴스 생성만 허용하지만 동일한 객체의 여러 인스턴스를 허용합니다. Singleton은 클라이언트가 여러 객체를 생성하지 못하도록 제한하고, 첫 번째 객체가 생성된 이후에는 첫 번째 객체를 반환합니다.

싱글톤 패턴을 사용한적이 없는 사람에게 싱글톤에 대한 예를 설명하는 것은 어렵습니다. 한 가지 예가 사무용 프린터를 사용하는 경우입니다. 사무실에 10명의 사람이 있고 그들은 하나의 프린터를 사용한다면, 10개의 컴퓨터가 하나의 프린터(instance)를 공유하는 것 입니다. 하나의 프린터를 공유함으로써 동일한 자원을 공유합니다.

var printer = (function () {

  var printerInstance;

  function create () {

    function print() {
      // underlying printer mechanics
    }

    function turnOn() {
      // warm up
      // check for paper
    }

    return {
      // public + private states and behaviors
      print: print,
      turnOn: turnOn
    };
  }

  return {
    getInstance: function() {
      if(!printerInstance) {
        printerInstance = create();
      }
      return printerInstance;
    }
  };

  function Singleton () {
    if(!printerInstance) {
      printerInstance = intialize();
    }
  };

})();

클라이언트가 create 메서드에 접근하는 것을 원하지 않기 때문에 이 메서드는 private 입니다. 하지만 getInstance 메서드는 public 입니다. 각 사무실 직원은 다음과 같이 getInstance 메서드를 이용하여 프린터 인스턴스를 생성할 수 있습니다.

var officePrinter = printer.getInstance();

AngularJS에서는 싱글톤(Singleton)이 널리 퍼져 있으며 가장 주목할만한 것은 services, factories, providers 입니다. 이들은 상태를 유지하고 자원 액세스를 제공하기 때문에 두 인스턴스를 생성하면 겹쳐지는 service/factory/provider 지점을 무효화시킵니다.

Race condition은 둘 이상의 스레드가 동일한 자원에 액세스하려고 시도할 때 다중 스레드 응용 프로그램에서 발생합니다. 싱글톤은 race condition에 영향을 받기 쉽기 때문에 처음에 인스턴스가 초기화되지 않으면 인스턴스화와 반환 대신에 두 개의 객체를 생성할 수 도 있습니다. 이는 singleton의 목적을 위배합니다. 따라서 개발자는 멀티 스레드 응용 프로그램에서 싱글톤을 구현할 때 동기화에 신경써야 합니다.

결론

디자인 패턴은 대규모 응용 프로그램에서 자주 사용되지만 다른 패턴보다의 장점을 이해하기 위해서는 연습이 필요합니다.

응용 프로그램을 작성하기 전에 각 요소가 다른 요소와 어떻게 상호작용하는지 철저히 생각해야합니다. Module, Prototype, Observer, Singleton 디자인 패턴을 리뷰한 후에야 이 패턴들을 인지하고 현업에서 사용할 수 있을 것입니다.

댓글