싱글톤 패턴은 개발 과정에서 가장 흔하고, 쉽게 접할 수 있는 디자인 패턴이다.
특별한 기능보다는 정말 보편적인 상황에서 사용될 수 있고, 간단한 구현 방법으로 이점을 얻을 수 있기 때문이 아닐까라고 생각한다. Spring DI/IoC, Controller, Service, DB Connection Pool 등, 동작 과정에 있어서 핵심적인 부분은 아니지만 내부적으로 singleton을 사용하고 있다.
이번에는 Singleton이 무엇이고, 왜 사용해야하는지, 그리고 어떻게 사용할 수 있는지에 대해 정리하려 한다.
Singleton이라는 단어에서 유추할 수 있듯이, 어플리케이션이 시작될 때 최초 한번만 객체 인스턴스를 생성하고, 이 인스턴스를 다른 곳에서 불러와서 사용하는 것을 의미한다.
예를 들어 DB Connection Pool처럼, 최초 실행 시에 생성한 객체를 필요할 때 불러오고 반환하는 구조에서 흔히 쓰인다.
다만, 싱글톤 내부적으로 가지고 있는 데이터가 예기치않게 변경되면 다른 요소에 그대로 영향을 미치고, 멀티쓰레드 환경에서 동기화 이슈도 발생하기 때문에 이를 잘 고려해서 사용해야한다.
그렇다면 싱글톤을 사용해야하는 이유는 무엇일까?
DB Connection Pool로 예시를 들어보자면 우리는 반드시 Thread Pool을 하나만 생성해서 그 내부적인 Thread들도 정의된 갯수만큼 초기화시켜 생성해야한다. 또한, 각각의 Connection들은 매번 동일한 connection 정보를 가지고 있기 때문에 메모리와 자원을 차지하면서 따로 만들어줄 필요가 없으며, 메모리에 직접 접근이 불가능한 경우, GC가 동작하지 않는 이상 메모리를 해제시켜줄 방법도 없다.
- 메모리 누수 및 낭비 최소화
- 하나의 객체로 데이터를 관리, 공유
- 단일 인스턴스 보장
싱글톤을 구현하는 방법은 간단하다.
멀티 쓰레드 환경에서도 정상적으로 동작하고, 메모리 낭비를 최소화하기 위해 여러 단계를 거치면서 구현 방법이 발전해왔는데, 최근에는 Initialization on demand holder idiom로 고착화되어 사용되고 있다.
단, 모든 방법에서 기본적으로 생성자를 private하게 유지하고, 인스턴스 변수를 내부에서 전역으로 관리하며, 외부에서 객체 생성없이 인스턴스를 받아오기 위해 public static으로 getInstance() 메소드를 정의하는 부분은 공통적으로 사용된다.
각각의 차이점은 결국 인스턴스 생성 시점이나 메서드 동기화 부분에 있다.
public class Singleton {
// 1. 객체가 유일해야하므로 외부에서 마음대로 생성하지 못하게끔 생성자를 private로 설정한다.
private Singleton() {}
// 2. JVM ClassLoader가 동작함과 동시에 전역 private 객체를 생성해서 클래스 스스로 인스턴스를 지니고 있는다.
private static Singleton instance = new Singleton();
// 3. 외부에서 생성된 인스턴스에 객체 생성없이도 접근할 수 있도록 public static 메소드를 추가한다.
public static Singleton getInstance() {
return instance;
}
}
어플리케이션이 실행됨과 동시에 JVM Class Loader가 클래스를 초기화하면서 바로 인스턴스를 생성하기 때문에 멀티 쓰레드 환경에서도 안전하다.
하지만 사용되지 않아도 반드시 싱글톤 객체가 생성되어 메모리를 계속 잡고 있다는 단점이 있다.
public class Singleton {
// 1. 객체가 유일해야하므로 외부에서 마음대로 생성하지 못하게끔 생성자를 private로 설정한다.
private Singleton(){}
// 2. Eager 방식과 달리, class 초기화 과정에서는 null값만을 지닌다.
private static Singleton instance;
// 3. 외부에서 생성된 인스턴스에 객체 생성없이도 접근할 수 있도록 public static 메소드를 추가한다.
// 단, 최초 호출 시에 인스턴스를 생성하고 반환해주는 과정이 추가되어 있다.
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
클래스 초기화 과정에서 인스턴스를 바로 생성하지 않고, 최초 사용 시에 생성함으로써 메모리가 낭비되는 현상을 방지했다.
하지만 멀티 쓰레드 환경에서 동일 시점에 getInstance()가 최초로 실행될 시, 2개 이상의 인스턴스가 생성될 수 있다는 단점이 있다.
public class Singleton {
// 1. 객체가 유일해야하므로 외부에서 마음대로 생성하지 못하게끔 생성자를 private로 설정한다.
private Singleton() {}
// 2. Singleton는 JVM에 의해 로드되면서 초기화 과정을 거치는데,
// LazyHolder에 대한 별도의 변수를 가지고 있지 않기 때문에 static class인
// LazyHolder는 최초 참조시점까지 초기화가 미뤄진다.
private static class LazyHolder {
public static final Singleton instance = new Singleton();
}
// 3. 외부에서 생성된 인스턴스에 객체 생성없이도 접근할 수 있도록 public static 메소드를 추가하여
// 최초 getInstance() 실행 시, 싱글톤 객체가 생성된다.
public static Singleton getInstance() {
return LazyHolder.instance;
}
}
사용하지 않아도 싱글톤 객체가 생성되는 Eager Initialization의 문제와 멀티쓰레드 환경에서 안전하지 않은 Lazy Initialization 문제를 해결한 방법으로, 현재까지 가장 많이 사용되는 싱글톤 구현 방법이다.
구현방법에는 추가로 Thread Safe, DCL, Enum 등을 활용한 방법이 있는데, 싱글톤 구현에 더이상 잘 사용되지 않고, 동기화와 관련된 부분이 많아 우선 고려하지 않았다.
다만, 개발자가 직접 동기화를 관리하기 보다는 JVM에 위임하여 처리하는 Initialization on demand holder idiom 방식이 고착화되었다는 부분만 알고 있으면 될 것 같다.