본문 바로가기
Architecture/Design Pattern

[디자인 패턴] 싱글턴 패턴(Singleton pattern)이란?

by Jman 2022. 4. 4.

인스턴스가 하나 뿐인 특별한 객체를 만들 수 있게 해주는 패턴입니다.

싱글턴 패턴은 다이어그램만 놓고 보면 그 어떤 패턴보다도 간단합니다.

클래스 다이어그램에 클래스가 달랑 하나 밖에 없으니깐요.

클래스 디자인 관점에서 보면 아주 단순하지만, 구현하는 데 쉽지 않습니다.

 

어떤용도로 사용할까요?

  • 스레드 풀
  • 캐시
  • 대화상자
  • 사용자 설정
  • 레지스트리 설정을 처리하는 객체
  • 로그 기록용 객체
  • 프린터
  • 그래픽 카드

같은 디바이스를 위한 디바이스 드라이버 같은 걸로 예로 들 수 있습니다.

 


 

 

 

public으로 선언된 거라면 클래스는 언제든 인스턴스를 만들 수 있습니다.

객체를 생성하는 방식은 new 연산자를 사용을 하면 된다는 걸 알고 계실겁니다.

만약 위와 같이 public  클래스로 선언하지 않은 클래스라면 같은 패키지 안에 있는 클래스에서만 인스턴스를 만들 수 있습니다.

 

하지만, 같은 패키지에 속한 클래스에서는 여전히 인스턴스를 두 개 이상 만들 수 있습니다.

 

하지만? 아래와 같은 코드라면?

public MyClass {
	private MyClass() {}
}

 

생성자가 private 으로 선언 되어있기 때문에 인스턴스를 만들 수 없는 클래스입니다.

private 생성자된 생성자를 사용할 수 있는 객체는 없습니다.

why?

생성자를 호출하려면 일단 그 클래스의 인스턴스가 있어야 되는데,

다른 클래스에서 이 클래스의 인스턴스를 만들 수 없기 때문에 인스턴스를 만드는 것이 불가능합니다.

MyClass 형식의 객체에서만 private 으로 선언된 생성자를 사용할 수 있고,

다른 어떤 클래스에서도 new MyClass() 라고 할 수 없습니다. 따라서, 인스턴스를 만들 수 없습니다.

 

public MyClass {
	public static MyClass getInstance() {
    
   }
}

 

위의 코드는 MyClass 에 정적메소드가 있습니다.

정적 메소드는

Myclass.getInstance();

위와 같은 방법으로 호출할 수 있습니다.

클래스 메소드라고 불립니다. 정적 메세드를 지칭할 때는 클래스 이름을 써야합니다.

 

이 두 가지를 합쳐봅시다!

public MyClass {
    private MyClass() {}
    
    public static MyClass getInstance() {
    	return new MyClass();
    }
}

이렇게 하면, 객체의 인스턴스를 만들 수가 있습니다.

 

자. 이제 마무리해서 의사코드가 아닌 제대로 인스턴스를 하나만 만들어질 수 있도록 싱글 패턴 구현 코드를 작성해보겠습니다.

public class Singleton {
	
    // 클래스의 유일한 인스턴스를 저장하기 위한 정적 변수입니다.
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
		if(uniqueInstance == null) { // uniqueInstance이 null이라면, 인스턴스가 생성되지 않았습니다.
        	// private으로 선언된 생성자를 이용해서 Single 객체를 만든 다음 uniqueInstance에 그 객체를 대입합니다.
            // 이렇게 하면 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않게 됩니다.
            // 이런 방식을 lazy instantiation 이라합니다.
        	uniqueInstance = new Singleton(); 
        }
        // uniqueInstance가 null이 아니면 이미 객체가 생성된 것입니다. 그런 경우는 바로 return 선언문으로 갑니다.
        return uniqueInstance;
	}
}

 

하지만, 하나의 인스턴스만을 생성하고 싶지만, 인스턴스가 하나가 아닌 그 이상이 만들어져 버리는 문제가 발생하기도 합니다.

그러한 문제는 스레드가 하나가 아니라,

다중 스레드일 경우에 위와 같은 코드를 구현을 해도 두 개 이상의 인스턴스를 생성합니다.

 

2개의 스레드가 돌 경우, JVM에서 아래와 같은 순서로 작업이 처리됩니다. 

[1번 스레드]

    public static Singleton getInstance() {

> uniqueInstance null

[2번 스레드]

    public static Singleton getInstance() {

> uniqueInstance null

[1번 스레드]

    if(uniqueInstance == null) {  

> uniqueInstance null

[2번 스레드]

    if(uniqueInstance == null) {

> uniqueInstance null

[1번 스레드]

    uniqueInstance = new Singleton(); 

> uniqueInstance  <object1>

        return uniqueInstance;

> uniqueInstance <object1>

[2번 스레드]

    uniqueInstance = new Singleton(); 

> uniqueInstance <object2>

        return uniqueInstance;

> uniqueInstance <obejct2>

 

 

위 와 같이 두 스레드가 코드를 읽고 흘러가게 되면, 인스턴스가 2개가 만들어지게 됩니다.

이 문제는

getInstance()를 동기화 시키기만 하면 멀티스레딩과 관련된 문제가 간단하게 해결됩니다.

 

아래에는 해결된 코드입니다.

public class Singleton {
	
    // 클래스의 유일한 인스턴스를 저장하기 위한 정적 변수입니다.
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    // getInstance() 에 synchronized 키워드만 추가하면
    // 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려합니다.
    // 즉, 두 스레드가 이 메소드를 동시에 실행 시키는 일은 일어나지 않습니다.
    public static synchronized Singleton getInstance() {
		if(uniqueInstance == null) { 
        	uniqueInstance = new Singleton(); 
        }
        return uniqueInstance;
	}
}

위와 같이 코드를 짜게되면, 동기화하여 속도에 문제가 생기게 됩니다.

 

사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐입니다.

일단 uniqueInstance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없습니다.

첫 번째 과정을 제외하면 동기화는 불필요한 오버헤드만 증가시킬뿐입니다.

 

그래서 몇 가지 방법을 소개하겠습니다.

  • getInstance() 의 속도가 그리 중요하지 않다면 그냥 둡니다.

> 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 됩니다. 동기화시크는 게 그리 어려운 일도 아니고 효율 면에서도 괜찮을 수 있습니다. 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억합시다. 만약, getInstance() 가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야 합니다.

  • 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버립니다.

> 애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행 중에 수시로 만들고 관리하기가 성가시다면 아래와 같은 식으로 처음부터 Singleton 인스턴스를 만들어 버리는 것도 괜찮은 방법입니다.

public class Singleton {
	// 정적 초기화 부분(static initialiger) 에서 Singleton의 인스턴스를 생성하면 스레드를 써도 문제 없습니다.
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton () {}
    
    public static Singleton getInstance() {
    	return uniqueInstance(); // 인스턴스가 존재하니, 그냥 리턴만하면 됩니다.
    }
}

이런 접근법을 사용하면 클래스가 로딩될 때, JVM에서 Singleton의 유일한 인스턴스를 생성해줍니다.

JVM에서 유일한 인스턴스를 새엇ㅇ하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.

 

  • DCL(Double-checking-Locking)을 써서 getInstance() 에서 동기화되는 부분을 줄입니다.

> DCL을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있씁니다.

이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 됩니다. (딱 이겁니다.)

아래 코드에 주석설명으로 추가 설명을 하겠습니다.

public class Signleotn {
	private volatile static Singleton uniqueInstance;
    
    public static Singleton getInstance() {
    	if (uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으로 들어갑니다.
        	synchronized (Singleton.class) { // 이렇게 하면, 처음에만 동기화가 됩니다.
            	if (uniqueInstance == null) {
                	//블록으로 들어온 후에도 다시 한 번 변수가 null인지 확인한 다음 인스턴스를 생성합니다.
                	uniqueInstance = new Singleton(); 
                }
            }
        }
    }
}

getInstance () 메소드를 사용할 때,

속도가 문제가 될 수 있다면 이런 식으로 Singleton 구현함으로써 오버헤드를 극적으로 줄일 수 있습니다.

 

여기서 잠깐,

volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Signleton 인스턴스를 초기화 되는 과정이 올바르게 진행되로록 할 수 있습니다.
(volatile 에 대해서는 추가로 블로그 작성 한 뒤에, 링크를 걸 예정)

 

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

[디자인 패턴] 팩토리 메소드 패턴  (0) 2022.12.01