싱글턴 패턴의 특징
싱글턴 패턴은 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공합니다.
- 싱글턴이 Lazy 방식으로 생성되도록 구현할 수 있습니다.
- 자원을 많이 잡아먹는 인스턴스에 특히 유용합니다 ( 애플리케이션이 시작될 때 객체가 생성되는 전역변수로 선언하는 것보다 자원을 아낄 수 있습니다.)
멀티스레딩 문제
- 멀티스레드 환경에서는 동시 접근 문제 때문에 의도치 않게 여러 인스턴스가 만들어질 수 있다.
- 단순히 public 접근자에서 인스턴스 유무를 가지고 판단하게 되면 인스턴스가 여러개 생성될 수 있다
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // <-- 멀티스레드 환경에서 동시에 진입 가능
instance = new Singleton();
}
return instance;
}
}
- 두 스레드가 동시에 getInstance()를 호출하면
instance == null 조건을 둘 다 통과 → 2개의 객체 생성될 수 있음.
멀티스레딩 문제 해결법
첫번째 방법. 간단하게 synchronized 동기화 하기 >> 이렇게 락을 걸면 성능이 100배 정도 저하가 된다.
이 부분이 병목으로 작용한다면 다른 방법을 생각해야함.
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
두번째 방법. 인스턴스를 처음부터 만드는 방식. >> lazy initialization(필요할 때 생성) 불가능.
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
// 클래스 로딩 시 JVM에서 하나뿐인 인스턴스를 생성한다.
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
세번째 방법.DCL(Double-Checked Locking) 을 써서 getInstance()에서 동기화되는 부분을 줄인다
class Singleton {
private static volatile Singleton instance; // volatile 중요!
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1차 체크
synchronized (Singleton.class) {
if (instance == null) { // 2차 체크
instance = new Singleton();
}
}
}
return instance;
}
}
- volatile을 붙여야 JVM의 명령 재정렬 문제를 방지.
- 처음만 synchronized 블록에 들어가고 이후엔 락이 없어 성능 좋음.
** JVM의 명령 재정렬 문제 란?
네번째 방법. enum을 사용한다. >> 간단하게 동기화 제, 클래스 로딩문제, 리플렉션, 직렬화 역직렬화 문제를 해결 할 수 있음
public enum Singleton {
UNIQUE_INSTANCE;
// 기타 필요한 필드
}
public class SingletonClient {
public static void main(String[] args) {
Singleton singleton = Singleton.UNIQUE_INSTACE;
// 여기서 싱글턴 사용
}
}
다섯번째 방법. Holder 패턴 (Lazy + Thread-safe)
class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- Holder 클래스는 getInstance() 호출될 때 로딩됨.
- JVM이 클래스 로딩을 thread-safe 하게 보장하므로 안전.
- Lazy initialization + 성능까지 잡은 방법
- 성능: DCL(double-checked locking)보다 간단하고 빠름.
- 단점
- 리플렉션 공격에 취약
- Singleton.class.getDeclaredConstructor().newInstance()로 접근해서 private 생성자를 강제로 호출할 수 있습니다.
- 그럼 Holder 안에 이미 만들어둔 인스턴스와 별개의 객체가 또 생깁니다 → 싱글턴 깨짐.
- 리플렉션 대응 방법
-
class Singleton { private static boolean instanceCreated = false; private Singleton() { if (instanceCreated) { throw new RuntimeException("Use getInstance() method to create"); } instanceCreated = true; } private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } - 첫 번째 생성자 호출 때 instanceCreated를 true로 설정 →
이후 리플렉션이 다시 생성자를 호출하면 RuntimeException 발생. - 하지만, 이 필드(instanceCreated) 자체도 리플렉션으로 조작이 가능해서 완벽 방어는 아님
-
- 직렬화(Serialization) 취약
- ObjectInputStream.readObject()를 쓰면 새 인스턴스가 만들어질 수 있습니다.
- 방어하려면 readResolve()를 구현해야 합니다
-
private Object readResolve() { return getInstance(); }
- 클래스 로딩 시점 제어 불가
- Lazy하다고 해도 결국 “처음 접근할 때 클래스 로딩”이 트리거인데,
ClassLoader 동작에 따라 예기치 않게 일찍 로딩될 수 있습니다. (대부분 문제는 아니지만, ClassLoader 복잡한 환경에선 고려 필요)
- Lazy하다고 해도 결국 “처음 접근할 때 클래스 로딩”이 트리거인데,
- 리플렉션 공격에 취약
'Java > DesignPattern' 카테고리의 다른 글
| 팩토리 메서드 패턴 vs 추상 팩토리 패턴 (0) | 2025.09.07 |
|---|---|
| 템플릿 메소드 패턴 (0) | 2024.06.03 |
| 팩토리 메서드 패턴 (0) | 2024.05.16 |
| MVVM 패턴 (0) | 2023.07.27 |