애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴
➡️ 하나의 인스턴스만 생성하여 사용하는 디자인 패턴
1. 메모리 측면
싱글톤 패턴을 사용하면 한 개의 인스턴스만들 고정 메모리 영역에 생성하고 추후 해당 객체를 접근할 때 메모리 낭비를 방지할 수 있다.
2. 속도 측면
생성된 인스턴스 사용 시, 이미 생성된 인스턴스를 사용하기 때문에 속도가 빠르다.
3. 데이터 공유
전역으로 사용하는 인스턴스이기 때문에 다른 여러 클래스에서 데이터를 공유하며 사용할 수 있다.
(동시성 문제 발생 주의)
싱글톤 인스턴스가 혼자 너무 많은 일을 하거나 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이 때 개방-폐쇄 원칙이 위배된다.
결합도가 높아지게 되면 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생한다.
➡️ 반드시 싱글톤이 필요한 상황이 아니면 지양하는 것이 좋음
public class Singleton {
// 단 1개만 존재해야 하는 객체의 인스턴스로 static으로 선언
private static Singleton instance;
// private 생성자로 외부에서 객체 생성을 막아야 한다.
private Singleton() {
}
// 외부에서는 getInstance() 로 instance 를 반환
public static Singleton getInstance() {
// instance 가 null 일 때만 생성
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
먼저 private static으로 Singleton 객체의 instance를 선언하고 getInstance() 메소드가 처음 실행 될 때만 하나의 instance가 생성되고 그 후에는 이미 생성된 instance를 return하는 방식으로 진행이 된다.
생성자를 private으로 생성하며 외부에서 새로운 객체의 생성을 막아줘야하는 것이 핵심이다.
// 같은 instance인지 Test
public class Application {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1);
System.out.println(singleton2);
}
}
/** Output
* vendingmachine.Singleton@15db9742
* vendingmachine.Singleton@15db9742
**/
➡️ 두 객체가 하나의 인스턴스를 사용하여 같은 주소 값을 출력하는 것 확인 가능
1. 여러 개의 인스턴스 생성
멀티스레드 환경에서 instance가 없을 때 동시에 아래의 getInstance() 메소드를 실행하는 경우 각각 새로운 instance를 생성할 수 있다.
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
2. 변수 값의 일관성 실패
다음과 같은 코드가 실행이 되었을 때 여러 개의 thread에서 plusCount()를 동시에 실행한다면 일관되지 않은 값이 생길 수 있다.
public class Singleton {
private static Singleton instance;
private static int count = 0;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void plusCount() {
count++;
}
}
1. 정적 변수에서 인스턴스 생성
static 변수로 singleton 인스턴스를 생성하는 방법으로 해결할 수 있다. 아래와 같이 초기에 인스턴스를 생성하게 된다면 멀티스레드 환경에서도 다른 객체들은 getInstance()를 통해 하나의 인스턴스를 공유할 수 있다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
2. synchronized 사용
synchronized를 적용하여 멀티스레드에서의 동시성 문제를 해결하는 방법이다.
하지만 해당 방법은 Thread-safe를 보장하기 위해 성능 저하가 발생한다.
public class Singleton {
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronzied Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
1. Lazy Initailization(게으른 초기화)
public class ThreadSafe_Lazy_Initialization{
// private static으로 인스턴스 변수 생성
private static ThreadSafe_Lazy_Initialization instance;
// private으로 생성자를 만들어 외부에서의 생성 막음
private ThreadSafe_Lazy_Initialization(){}
// synchronized 동기화를 활용해 스레드를 안전하게 만듦
// ➡️ 성능 저하 발생
public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
return instance;
}
}
2. Lazy Initialization + Double-checked Locking
1번의 성능 저하를 완화시키는 방법
public class ThreadSafe_Lazy_Initialization{
private volatile static ThreadSafe_Lazy_Initialization instance;
private ThreadSafe_Lazy_Initialization(){}
public static ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null) {
synchronized (ThreadSafe_Lazy_Initialization.class){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
}
}
return instance;
}
}
조건문으로 인스턴스의 존재 여부를 확인한 다음 두번째 조건문에서 synchronized를 통해 동기화시켜 인스턴스를 생성하는 방법
스레드를 안전하게 만들면서, 처음 생성 이후에는 synchronized를 실행하지 않기 때문에 성능 저하 완화 가능
➡️ 완전히 완벽한 방법은 X
3. Initialization on demand holder idiom (holder에 의한 초기화)
클래스 안에 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법
public class Something {
private Something() {
}
private static class LazyHolder {
public static final Something INSTANCE = new Something();
}
public static Something getInstance() {
return LazyHolder.INSTANCE;
}
}
2번처럼 동기화를 사용하지 않는 방법을 안하는 이유는 개발자가 직접 동기화 문제에 대한 코드를 작성하면서 회피하려고 하면 프로그램 구조가 그만큼 복잡해지고 비용 문제가 발생할 수 있고 코드 자체가 정확하지 못할 때도 많다.
따라서 3번과 같은 방식으로 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용해 싱글톤의 초기화 문제에 대한 책임을 JVM에게 떠넘기는 것을 활용한다.
클래스 안에 선언한 클래시은 holder에서 선언된 인스턴스는 static이기 때문에 클래스 로딩 시점에서 한 번만 호출된다. 또한 final을 사용해서 다시 값이 할당되지 않도록 만드는 방식을 사용한 것
➡️ 실제로 가장 많이 사용되는 일반적인 싱글톤 클래스 사용 방법이 3번
[참고 자료]