블로그 내 검색

2012. 2. 21.

자바스크립트 패턴 #1 - 싱글톤 패턴 (JavaScript's Singleton Pattern) (수정)

자바스크립트의 함수는 new 로 생성자로 사용할 때마다 새로이 생성된 객체를 리턴합니다.

하지만 특수한 상황에서는 하나의 함수에서 생성되는 객체가 오직 한개만을 가져야 할 때가 있습니다. 그럴 경우 사용되는 디자인 패턴이 Singleton Pattern 입니다.

보통 하나의 객체를 전역적으로 공유해야 할 때 많이 쓰이지요.

자바스크립트에서는 그냥 전역영역에 객체를 하나 생성해서 두면 되지만 전역 객체의 사용은 매우 안좋은 코딩이며 절대 사용해서는 안되는 것입니다.

Java의 경우에는 생성자를 private 로 지정하고 별도의 스태틱 팩토리 메서드를 통해 인스턴스를 반환받는 형식으로 구현됩니다. 아래 처럼요.
public class JavaSingleton {

    // private 접근제한으로 객체를 미리 생성.
    // 간혹 생성자에서 늦은 초기화로 생성하기도 하나,
    // 객체의 실제 생성시간 차이로 인한 멀티스레딩 오류가 있으므로 비추천.
    private static JavaSingleton ME = new JavaSingleton();

    // private 생성자
    private JavaSingleton() { }

    // 스태틱 팩토리 메서드.
    // 언제나 하나의 객체만 리턴.
    public static JavaSingleton getInstance() {
         return  JavaSingleton.ME;
    }
}

public class JavaSingletonTester {
    public static void main(String [] args) {
     
        // 일반적인 new 생성 불가능.
        // JavaSingleton single = new JavaSingleton();

        // 두 객체는 동일.
        JavaSingleton ins1 = JavaSingleton.getInstance();
        JavaSingleton ins2 = JavaSingleton.getInstance();   

    }
}
자바스크립트의 경우 위 방법으로는 구현이 되지 않습니다.
명시적인 private 선언도 없고, 저런 생성자 자체를 막는 방법도 존재하지 않기 때문이죠.

다른 방법을 생각해봐야 합니다.

만일 함수가 생성자로 호출된다면 자신이 생성할 객체를 따로 가지고 있다가, 재차 호출 시 그 객체만을 반환하게 하는 방식을 생각해 봅시다.
이러한 방식이 될 것입니다.
// 싱글톤을 생성해주는 모듈 객체. 
// 익명 함수 실행 결과를 받습니다.
var SingletonTester = (function(){
    
    // 실제 싱글톤 적용 객체
    function Singleton(args) {
        
        // 내부 작업...
        var args = args || {};
        this.a = args.a;
        this.b = args.b;  
    }

    // 인스턴스 객체. 
    // 다수의 객체 생성을 제한하는 역할입니다
    var INSTANCE;

    // 외부에 공개될 객체를 반환합니다.
    // 모듈 패턴(Module Pattern)이라고 부릅니다
    return {        
        getInstance: function ( args ){
            if (INSTANCE === undefined) {
                instance = new Singleton( args );
            }
            return INSTANCE;
        }
    };
    
})(); // () 연산자로 선언과 동시에 바로 실행.

// 테스트
var singletonTest = 
    SingletonTester.getInstance( { a: "hello" } );
singletonTest.a; // a 
 - 위 코드는 Essential JavaScript Design Patterns For Beginners 를 참고 하였습니다.
위 방법대로 하면 getInstance를 사용하는 한 언제나 동일한 객체를 얻어낼 수 있습니다.
여기서 좀더 욕심을 부려 봅시다.

위 방식은 싱글톤을 적용할 객체마다 위의 코드를 써줘야 한다는 단점과 반드시 getInstance 함수를 사용하여 객체를 얻어내야 한다는 번거로움이 있고, 실수로라도 new 연산자를 사용할 경우 엉뚱한 객체가 얻어지기도 합니다.

위의 단점을 해소한 재사용이 가능하며, new를 쓰든 getInstance를 사용하든지간에 싱글톤이 적용가능하도록 코드를 약간 다르게 구현해 봅시다.

재사용하려면 몇가지 고려가 필요한데, 함수를 전달하여 그것을 싱글톤 생성자로 바꿔주려면 인자를 유연하게 넘겨줘야 하며, 동적으로 getInstance 함수도 지정해줘야 합니다.
그리고 private으로 관리되는 유일 객체 변수도 가져야 합니다.

생성자의 인자 전달 문자는 arguments 함수와 apply를 사용한 기법으로, 유일 객체 변수는 클로저화 시키는 방법밖엔 없는 것 같습니다.

아래 코드로 가기 전에 자바스크립트에서 new 연산자의 동작을 한번 봅시다.
  1. 생성자의 프로토타입을 연결한 새 빈 객체를 생성한다.
  2. 전달된 인자와 함께 생성된 객체를 컨텍스트로 함수를 실행한다.
  3. 함수 실행 결과가 객체이면 그 객체를 리턴하고, 아닐 경우 위에서 생성한 객체를 리턴한다.
아래는 코드.
/* 
    @name Singletonify - By javarouka (MIT Licensed)
*/
var Singletonify = function(cons) {
    
    // 유일 객체 변수
    var INSTANCE;
    
    // 클로저 생성
    var c = function() {
        // 유일 객체가 정의되지 않았다면 객체를 생성.
        if(INSTANCE === undefined) {
            
            // 여기서부터 new 연산자의 내용을 흉내냅니다.
            
            // 새 함수를 선언하고 인자로 전달받은 함수의 프로토타입으로 연결합니다.
            var F = function() {};
            F.prototype = cons.prototype;
            
            // 객체를 생성하고 생성된 객체를 컨텍스트로 호출합니다.            
            var t = new F();
            var ret = cons.apply(t, Array.prototype.slice.call(arguments));
            
            // 이때, 반환값이 객체이면 객체를, 아니라면 위의 객체를
            // 생성 객체로 지정합니다.
            INSTANCE = (typeof ret === 'object') ? ret : t;             
        }
        
        // 객체를 리턴합니다.
        return INSTANCE;
    }

    // 팩토리 메서드로도 접근할 수 있게 합니다
    c.getInstance = function() {
        return c.apply(null, Array.prototype.slice.call(arguments));
    }

    // 생성자를 대체한 클로저를 리턴
    return c;
};

// 테스트 함수
function javarouka(value) {
    this.v = value;
}

// 싱글톤화
var Single = Singletonify(javarouka);

// 테스트
var s1 = Single.getInstance("hello");
var s2 = new Single("javascript");
var s3 = new Single("world");

console.log(s1 === s2); // true
console.log(s2 === s3); // true

console.log(s1.v); // hello
console.log(s2.v); // hello
console.log(s3.v); // hello
위 코드의 포인트는 new를 사용한 객체 생성을 흉내내는 것과, 함수의 동작을 덮어 써버리는 것입니다.
좀 매끄럽지 않고 억지스러운 부분도 있는것 같습니다.
코드상 오류나 더 좋은 방법이 있다면 리플로 ...

참고자료

포스팅에는 아래 링크의 자료를 많이 참고했습니다.

작은 자유:  javascript singleton (자바스크립트 싱글턴)
http://ohgyun.com/248

Essential JavaScript Design Patterns For Beginners
http://addyosmani.com/resources/essentialjsdesignpatterns/book/#singletonpatternjavascript

Rhio.Kim's Blog: javascript singleton(자바스크립트 싱글 패턴) story
http://rhio.tistory.com/tag/%EC%8B%B1%EA%B8%80%ED%86%A4

댓글 13개:

  1. 싱글톤에 대해 배우다가 긴가민가 하는중인데 보고
    도움이 되네요 감사합니다

    답글삭제
    답글
    1. 사실 위에 쓴 방법은 배보다 배꼽이 큰 방법같아서 아쉽습니다;

      댓글 감사합니다!

      삭제
  2. 음... 근데 Singletonify객체도 객체니까 결국에는 전역에 객체를 선언한게 되지 않을까요?

    답글삭제
    답글
    1. 전역에 선언을 피하려면 모듈화를 적용해야 하지요.

      포스트에서는 편의상 전역에 선언했지만 여러 방법으로 모듈로 빼낼 수 있습니다.

      예를 들면...

      me.javarouka 이란 네임스페이스를 설정되어 있고, 함수 관련 모듈이니
      me.javarouka.Function 로 모듈을 export 하려고 한다면,

      (function(exports) {

      exports.Singletonify = /* Singletonify 구현체 */

      })(me.javarouka.Function);

      이런 식으로 하거나
      commonjs 등의 표준 환경이라면

      /* require 구문... */

      exports.Singletonify = /* ... */;

      requirejs를 쓰신다면

      // me/javarouka/utils/Function.js 파일로 저장
      define(function() {

      /* private 멤버들... */

      /* 기타 Function 관련 유틸 구현 */

      var Singletonify = /* Singletonify 구현 */

      return {
      // ... 공개할 것들...
      Singletonify: Singletonify
      }
      });

      이런 식으로 하면 될거 같습니다.

      삭제
  3. 감사합니다. 도움이 많이 되었습니다.^^

    답글삭제
  4. 음 질문 있습니다만... 단순히 싱글톤이라 하면 단순히 스크립트 내부에
    var singleton = {} 식으로만 써도 되지 않을까요? ^^;;

    이 이후로는 절대 해당 인스턴스를 생성할 수가 없으니까요 ㅎㅎ;
    자바스크립트에서는 함수에 이름 붙이는 순간부터 싱글톤이라 불릴 수 없다는 생각을 해 봅니다.

    좀 더 고민할 점이 있다면 캡슐화 정도의 문제겠군요 ㅎㅎ;

    답글삭제
    답글
    1. 위에 포스팅한 내용은 js에서 싱글톤을 구현해서 뭔가 여기저기 널리 이롭게 사용하겠다! 이런 의도는 아닙니다.

      단순히 js에서 어떻게 구현할 수 있나 살펴본 것일 뿐이지요. 말씀대로 사실 js에 싱글톤이 의미가 있나 싶습니다.

      한가지 다른 점은 위 소스대로 구현할 경우 타 new 연산자를 붙인 객체 생성도 전부 싱글톤화가 된다 정도겠네요
      이렇게 해도 별 달라지는 건 없지만요.

      댓글 감사합니다 :)

      삭제
  5. 너무 좋은 내용 감사드립니다 ^^

    한가지 질문드릴 부분이 있는데요, c function 부분에서 new생성자 부분을 prototype을 연결하는 형태로 정의한 이유가 있을까요?

    var Singletonfy = function( cons ){

    var INSTANCE;

    var c = function(){

    if( INSTANCE === undefined ){

    INSTANCE = new cons( arguments[ 0 ] );

    }

    return INSTANCE;

    }

    c.getInstance = function(){

    return c.apply(null, Array.prototype.slice.call( arguments ) );

    }

    return c;

    }

    function cons1(){

    for( var a in arguments[ 0 ] ){

    this[ a ] = arguments[ 0 ][ a ];

    }

    }

    var single = Singletonfy( cons1 );

    var s1 = new single({ a : 1, b : 1 });
    var s2 = new single({ a : 2, b : 2 });
    var s3 = single.getInstance({ a : 3, b : 3});

    console.log( s1 === s2 );
    console.log( s2 === s3 );
    console.log( s3 );

    이런식으로 바로 new 로 생성해서 인스턴스를 넘겨줘도 되지 않을까요? ^^ 추가로 arguments를 객체 형태로 받을수 있게도 한번 해봤습니다.

    답글삭제
  6. 작성자가 댓글을 삭제했습니다.

    답글삭제
  7. 자바스크립트 첫번째 예제에
    instance = new Singleton( args ); 부분의 instance가 대문자로 수정되어야 할것같습니다.

    답글삭제