S E P H ' S

[생성 패턴] 싱글톤(Singleton) 패턴 본문

Programing & Coding/Design Pattern

[생성 패턴] 싱글톤(Singleton) 패턴

yoseph0310 2023. 3. 4. 20:42

생성(Creational) 패턴

생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴이다.

생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리한다.

생성 패턴은 시스템이 상속(Inheritance)보다 복합(composite)방법을 사용하는 방향으로 진화되어 가면서 더욱 중요해지고 있다.

 

생성 패턴에는 두 가지 중요한 이슈가 있다.

  1. 생성 패턴은 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화 한다.
  2. 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려준다.

요약하면, 생성 패턴을 이용하여 무엇이 생성되고, 누가 이것을 생성하고, 어떻게 생성되는지 언제 생성할 것인지 결정하는 것에 대한 유연성을 확보할 수 있다.


싱글톤(Singleton) 패턴

싱글톤 패턴은 클래스의 인스턴스가 오직 하나임을 보장하고 이 인스턴스에 접근할 수 있는 전역적 접촉점을 제공하는 패턴이다.

프로그램 시작부터 종료까지 어떤 클래스의 인스턴스가 메모리 상에 단 하나만 존재할 수 있게 하고 인스턴스에 대해 어디에서나 접근할 수 있도록 하는 패턴이다.

 

왜 고안되었을까?

개발을 하다보면 어떤 클래스에 대해서 단 하나의 인스턴스만을 갖도록 하는 것이 좋은 경우가 있다.

예를 들어, 로그를 찍는 객체나 쓰레드 풀, 윈도우 관리자 등 여러 객체를 관리하는 역할의 객체는 단 하나의 인스턴스를 갖는 것이 바람직 하다.

 

하나의 인스턴스에 어떻게 접근할 수 있을까?

전역 변수가 생각이 나지만 그보다는 클래스가 자신의 유일한 인스턴스로 접근하는 방법을 자체 관리하는 것이다. 생성자를 private하게 만들어 외부에서는 인스턴스를 생성하지 못하게 하고 내부에서 단 하나의 인스턴스를 생성하여 외부에는 그 인스턴스에 대한 접근 방법을 제공할 수 있다.


싱글톤(Singleton) 구현 방법

싱글톤 패턴을 구현하는 방법은 다양하다. 하지만 각각 방법마다 공통적으로 갖는 특징이 있다.

  1. private 생성자만을 정의하여 외부 클래스로부터 인스턴스 생성을 차단한다.
  2. 싱글톤을 구현하고자 하는 클래스 내부에 멤버 변수로써 private static 객체 변수를 만든다.
  3. public static 메소드를 통해 외부에서 싱글톤 인스턴스에 접근할 수 있도록 접점을 제공한다.

 

1. Eager Initialization

가장 간단한 형태의 구현 방법이다. 이는 싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법이다. 그러나 애플리케이션에서 해당 인스턴스를 사용하지 않더라도 인스턴스를 생성하기 때문에 낭비가 발생할 수 있다.

 

public class Singleton {
	private static final Singleton instance = new Singleton();
    
    	private Singleton() {}
    
        public static Singleton getInstance() {
            return instance;
        }
}

 

Eager Initialization을 사용할 때는 싱글톤 클래스가 다소 적은 리소스를 다룰때여야 한다.

File System, Database Connection 등 큰 리소스를 다루는 싱글톤을 구현할 때는 위와 같은 방식보단 getInstance()가 호출될 때까지 싱글톤 인스턴스를 생성하지 않는 것이 더 좋다. 그리고 Eager Initialization은 Exception에 대한 Handling도 제공하지 않는다.

 

 

2. Static Block Initialization

Static Block Initialization은 1번과 유사하지만 static block을 통해서 Exception Handling에 대한 옵션을 제공한다.

 

public class Singleton {
	
    private static Singleton instance;
    
    // lombok을 사용한다면 @NoArgsConstructor 사용 가능
    private Singleton() {}
    
    static {
    	try {
        	instance = new Singleton();
        } catch (Exception e) {
        	throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static Singleton getInstance() {
    	return instance;
    }

}

 

위와 같은 경우 싱글톤 클래스의 인스턴스를 생성할 때 발생할 수 있는 예외 처리는 가능하지만, Eager Initialization과 마찬가지로 클래스 로딩 단계에서 인스턴스를 생성하기 때문에 여전히 큰 리소스를 다루는 경우에는 적합하지 않다.

 

 

3. Lazy Initialization

Lazy Initialization은 이름에 걸맞게, 나중에 초기화하는 방법이다.

이는 global access한 getInstance() 메소드를 호출할 때 인스턴스가 없다면 생성한다.

 

public class Singleton {
	
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (instance == null) {
        	instance = new Singleton();
        }
        return instance;
        
    }
}

 

Lazy Initialization을 사용하면 1, 2번의 문제였던 문제(사용하지 않았을 경우 인스턴스가 낭비)에 대해 어느정도 해결이 된다. 그러나 이 경우도 multi-thread 환경에서 동기화 문제가 발생할 수 있다.

만약 인스턴스가 생성되지 않은 시점에서 여러 쓰레드가 동시에 getInstance()를 호출하게 되면 예상치 못한 결과를 얻을 수 있고, 단 하나의 인스턴스를 생성한다는 싱글톤 패턴에 위반하는 문제점이 야기될 수 있다. 그래서 Lazy Initialization 방법을 사용해도 괜찮을 때는 single-thread 환경이 보장됐을 때 입니다.

 

4. Thread Safe Singleton

이 방법은 3번의 multi-thread 환경에서의 문제점을 해결하기 위한 방법으로 getInstance() 메소드에 synchronized를 추가하는 방식이다. synchronized 키워드는 임계영역(Critical Section)을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능케 해준다.

 

public class Singleton {
	
    private static Singleton instance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
    	if (instance == null) {
        	instance = new Singleton();
        }
        return instance;
        
    }
}

 

이와 같이 구현하면 getInstance() 메소드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 스레드 환경에서도 정상 동작한다. 그러나 synchronized 키워드 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 애플리케이션에서는 성능이 떨어지게 된다.

 

그래서 고안된 방식이 double checked locking이다.

이는 getInstance() 메소드 수준에 lock을 걸지 않고 instance가 null 일 경우만 synchronized가 동작하도록 한다.

 

public class Singleton {
	
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
    	
        return instance;
    }
}

 

5. Bill Pugh Singleton Implementation

Bill Pugh가 고안한 방식으로, inner static helper class 를 사용하는 방식이다.

앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식으로, 현재 가장 널리 쓰이는 싱글톤 구현 방법이다.

 

public class Singleton {

	
    private Singleton () {}
    
    private static class SingletonHelper {
    	private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
    	return SingletonHelper.INSTANCE:
    }
}

 

private inner static class 를 두어서 싱글톤 인스턴스를 갖게 한다.

이때 1, 2번과 차이점은 SingletonHelper 클래스는 Singleton 클래스가 로드될 때에도 Load 되지 않다가 getInstance가 호출됐을 때 비로소 JVM 메모리에 로드되고 인스턴스를 생성한다. 더군다나 synchronized를 사용하지 않아 4번의 문제인 성능저하도 해결된다.

 

 

6. Enum Singleton

1~5번 싱글톤 모두 완전히 안전할 수는 없다. Java의 * Reflection에 의해 싱글톤이 파괴될 수 있기 때문이다. 

 

public enum EnumSingleton {
	INSTANCE;
    
    public static void doSomething() {
    
    }

}

 

하지만 이 또한 사용하지 않았을 경우 메모리 문제와 유연성이 떨어진다는 한계를 지니고 있다.


싱글톤 구현에도 여러 구현 방식이 있고 각각의 방식마다 장단점이 있지만 5번의 inner static class 방식이 최선이라고 생각된다. 

 

 

출처 : https://readystory.tistory.com/116