[05] Singleton Pattern

2022. 1. 4. 22:29Design Pattern

Singleton Pattern

  1. 하나뿐인 객체를 의미함
  2. Application에서 실행 후 최초 한번만 메모리에 할당 후 그 메모리에 Instance를 만들어 사용함
  3. 생성자가 여러번 호출되어도 실제로 생성되는 객체는 하나이고, 최초 생성이후 호출된 생성자는 최초에 생성한 객체를 반환함.
    (자바에서는 생성자를 Private으로 만들어 생성을 불가하게 하고, getInstance(); 로 받아서 쓰기도 함)

Why to use Singleton Pattern

  1. 고정된 메모리 영역을 얻으면서 한번의 생성으로 Instance를 메모리에 할당해 쓰기 때문에 이 후 메모리 낭비 방지 가능
  2. Singleton으로 만들어진 객체(Class)의 InstancePublic Instance 이기 때문에 다른 객체(Class)의 Instance들이 데이터를 공유받기 쉬움
  3. 두번째 이용부터는 객체(Class) 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점이 있음

When will use Singleton Pattern

  • DBCP(Database Connection Pool), Logger와 같이 공통된 객체를 여러번 사용할 때 많이 사용된다.

What is the problem with the Singleton Pattern?

  1. Singleton Instance가 너무 많은 일을 하거나, 많은 데이터를 공유 시킬 경우 다른 객체의 Instance 와 결합도가 높아져 OCP(개방-폐쇄)원칙에 위배됨.
    → 이는 즉 객체지향 설계원칙에 어긋나는 것이며, 수정이 어려워지고 테스트가 어려워진다.
  2. 다중 스레드에서 경쟁 상태(Race condition)를 발생시키는 경우 패턴이 깨지게 된다.
    (경합 조건 : 메모리와 같이 동일한 자원을 2개 이상의 스레드가 이용하려고 경합하는 현상)
    (1) Instance가 할당되지 않았을 때 Thread1Instance를 생성하고 메모리에 할당하려고 함.
    (2) Thread1이 생성자를 호출하기 전(메모리에 할당 전) Thread2가 생성 후 메모리에 할당해버리면 2개의 Instance가 생성

How to use Singleton Pattern

(JAVA)

public class Foo {
    private Foo() {
    }
 
    private static class LazyHolder {
        public static final Foo INSTANCE = new Foo();
    }
 
    public static Foo getInstance() {
        return LazyHolder.INSTANCE;
    }
}

위 나온 문제점중 경합조건을 보완하기 위해 나온 방법으로, Holder에 의한 초기화(Initialization on demand holder idion)를 사용하면 된다.

클래스 안에 클래스(Holder)를 두어 JVM의 Class loader 매커니즘과 Class가 로드되는 시점을 이용한 방법이다.

이 방법은 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용하여 싱글턴의 초기화 문제에 대한 책임을 JVM에 떠넘긴다.
Holder안에 선언된 Instance가 static이기 때문에 클래스 로딩시점에 한번만 호출될 것이며 final을 사용해 다시 값이 할당되지 않도록 만든 방법이다.
(가장 많이 사용하고 일반적인 Singleton Pattern 사용 방법이다.)

 

이 외에도 여러 방법이 있기는 하나, 위 방법이 가장 일반적이기에 위 방법만 작성한다.

핵심은 개발자가 직접 동기화 문제에 대해 코드를 작성하고, 문제를 회피하려 한다면 프로그램 구조가 그만큼 복잡해지고 개발자의 시간 비용이 더 많이 사용된다.

그렇기 때문에 이것을 JVM에게 떠넘기는 것이다.

 

(Javascript ES6+)

자바스크립트에서는 객체 리터럴을 사용하면 Singleton을 구현한 것이다.

const obj = {
  name: "y0ngha"
}

하지만 이 객체는 캡슐화가 되어 있지 않기 때문에 결합도가 커지며 데이터를 보호하지 못한다.

따라서 우리는 Singleton을 구현할 때 캡슐화를 진행해주어야 한다.

 

ES5도 물론 사용하는 방법이 있으나 따로 작성하진 않겠다.(Closure, IIFE를 이용하면 된다.)

 

정적(static) 필드 기능이 추가된 이후로는 아래와 같이 사용하면 된다.

class Foo {
  static instance;
  constructor() {
    if (instance) return instance;
    instance = this;
  }
}

const foo1 = new Foo();
const foo2 = new Foo();

console.log("is Same ==> ", foo1 === foo2);

Javascript에서는 경합조건 현상을 어떻게 보완할까?

→ 당연히 하지 않는다. Javascript는 단일 스레드 언어다.

 

하지만, Worker나 특정 패키지를 통해 다중 스레드를 구현할 경우 이는 개발자가 동기화에 고민해야하고 조심해야한다.

어찌보면 전역 상태관리를 도와주는 패키지인 mobX, Redux도 Singleton pattern을 활용한다고 볼 수 있다.

 

Angular에서는 Singleton이 많이 사용되고 있다.
가장 주목할만한 것은 Service, Factory 및 Provider다. 
상태를 유지하고 리소스 액세스를 제공하기 때문에 두 개의 인스턴스를 생성하면 Service/Factory/Provider의 공유 시점이 무효화된다.

'Design Pattern' 카테고리의 다른 글

[04] SOLID 원칙  (0) 2021.12.09
[03] MVC, Flux Pattern  (0) 2021.12.09
[02] Presentational & Container Pattern  (0) 2021.12.09
[01] Facade 패턴  (0) 2021.10.26