싱글톤 패턴
싱글톤 패턴(Singleton Pattern)
1. 싱글톤 패턴 개요
싱글톤 패턴은 특정 클래스에 대해 단 하나의 인스턴스만 존재하도록 보장하는 디자인 패턴입니다. 이 패턴은 메모리 최적화와 자원 공유가 필요한 경우에 유용합니다.
- 주요 특징:
- 하나의 인스턴스만 존재: 동일 클래스에서 객체가 중복 생성되지 않도록 보장
- 전역 접근: 애플리케이션 어디서든 동일한 객체를 접근 가능
- 객체 재사용: 한 번 생성된 인스턴스를 재사용하여 리소스 절약
2. 싱글톤 패턴 구현 원리
- 생성자 private 설정 : 외부에서
new
로 인스턴스를 생성할 수 없도록 제한 getInstance()
메서드 이용: 이미 생성된 인스턴스를 반환하는식으로 구성
3. 싱글톤 패턴 구현 기법
모두 싱글톤을 지향한다는 점은 같지만 장단점이 존재합니다. 각 코드 기법들은 단점들이 존재하며, 최종적으로는 6번, 7번을 사용하여 싱글톤을 구현하는것을 권장합니다.
- Eager Initialization
- Static block initialization
- Lazy initialization
- Thread safe initialization
- Double-Checked Locking
- Bill Pugh Solution
- Enum
4. 각 기법 설명
(1) Eager Initialization
- 한번만 미리 만들어두는, 가장 직관적인 방법
- 객체가 사용 되기 전에 즉시 초기화가 이루어져, 애플리케이션 실행 초기에 메모리 적재
class Singleton {
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static final Singleton INSTANCE = new Singleton();
// 생성자를 private로 선언 (외부에서 new 연산자 X)
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
- 장점
멀티 쓰레드 환경
에서도 안전 : 인스턴스가static final
변수
- 단점
- 메모리 낭비 : 인스턴스를 사용하지 않더라도 메모리에 적재
- 예외 처리 불가 : 애플리케이션의 시작 시점에 싱글톤 인스턴스 생성에 실패한 경우
(2) Static block Initialization
static block
을 이용해 예외를 잡는 방법- static block : 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록을 말합니다.
- 싱글톤 인스턴스는 클래스가 로딩 되는 시점에 생성
class Singleton {
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance; // final X
// 생성자를 private로 선언 (외부에서 new 사용 X)
private Singleton() {}
// static 블록을 이용해 예외 처리
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
- 장점
- 예외 처리 가능 : static block
- 단점
- (이론상) 메모리 낭비 : 클래스 로딩과 동시에 객체가 생성되기 때문에 초기화 시점과 실제 사용 시점이 맞지 않으면 메모리 낭비 이슈 존재
(3) Lazy Initialization
- 객체 생성에 대한 관리를 내부적으로 처리하는 방식
- 외부에서
getInstance()
를 호출 했을 때, 초기화 진행- 인스턴스 변수의 null 유무에 따라 초기화 하거나 이미 만들어진 인스턴스를 반환하도록 설계
class Singleton {
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// 생성자를 private로 선언 (외부에서 new 연산자 X)
private Singleton() {}
// 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행 (lazy)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 오직 1개의 객체만 생성
}
return instance;
}
}
- 장점
- 초기화 지연 : 위의 자원낭비(메모리 낭비)의 한계점을 극복
- 단점
Thread Safe
하지 않음 : 멀티쓰레드 환경에서 싱글톤을 지킬 수 없음스레드 A
,스레드 B
가 존재한다고 가정스레드 A
가 If문을 평가하고 인스턴스 생성 코드로 진입 (instance가 null)쓰레드 A
가 인스턴스를 생성하기 전에스레드 B
가 If문을 평가하게 된다면?스레드 B
가 If문을 평가하는 시점에도 instanc가 null이기 때문에 인스턴스 생성 코드로 진입- 결과적으로 스레드 A와 B가 인스턴스 초기화 코드를 두번 실행 (원자성 결여)
- Test Code
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class SingletonTest {
@Test
void multi_thread() throws InterruptedException {
// 여러 스레드를 만들어서 동시에 Singleton.getInstance() 호출
Runnable task = () -> {
Singleton instance = Singleton.getInstance();
System.out.println("Thread: " + Thread.currentThread().getName() + " - Instance: " + instance);
// 객체 주소가 동일한지 확인 (모든 스레드에서 동일한 객체를 참조해야 함)
assertSame(Singleton.getInstance(), instance);
};
// 10개의 스레드 실행
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
// 모든 스레드가 끝날 때까지 기다리기
for (int i = 0; i < 10; i++) {
threads[i].join();
}
}
}
- 결과
(4) Thread safe initialization
-
getInstance()
메서드synchronized
키워드를 사용: 쓰레드들이 하나씩 접근하여 인스턴스를 반환받음synchronized 키워드는 멀시 쓰레드 환경에서 각 쓰레드가 순차적으로 접근하도록 합니다. 즉 쓰레드가 해당 메서드를 실행하는 동안 다른쓰레드가 접근하지 못하도록 lock을 걸어서 경쟁상태(race condition)이 발생하지 않도록 합니다.
class Singleton {
private static Singleton instance;
private Singleton() {}
// synchronized 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 장점
- Thread Safe
- 단점
- 동기화 처리작업으로
Overhead
발생
- 동기화 처리작업으로
(5) Double-Checked Locking
- 인스턴스를 호출할 때마다
synchronized
동기화가 실행되지 않고, 최초 초기화시에만 동기화가 실행되도록 설계 - 인스턴스 필드에
volatile
키워드를 붙여주어야 함
class Singleton {
private static volatile Singleton instance; // volatile 키워드 적용
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 메서드에 동기화 거는게 아닌, Singleton 클래스 자체를 동기화 걸어버림
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton(); // 최초 초기화만 동기화 작업이 일어나서 리소스 낭비를 최소화
}
}
}
return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
}
}
- 장점
- 성능 향상 : 이미 인스턴스가 생성된 경우 동기화 작업이 필요 없음
- 단점
Thread Safe
이슈 : JVM에 따라서 여전히 Thread Safe 하지 않은 경우가 발생하기 때문에 사용을 지양
(6) Bill Push Solution(LazyHolder)
- 권장되는 두가지 방법 중 하나
- 클래스 안에 내부 클래스(holder)를 두어 클래스가 로드되는 시점을 이용한 방법
- 내부 클래스인
SingleInstanceHolder
는getInstance()
호출시에 클래스 로드 및 초기화
- 내부 클래스인
- 멀티쓰레드 환경에서 안전하고
Lazy Loading
(나중에 객체 생성)도 가능한 완벽한 싱글톤 기법 - static 메서드에서는 static 멤버만 호출 할 수 있기 때문에 내부 클래스를 static으로 설정
class Singleton {
private Singleton() {}
// static 내부 클래스를 이용
// Holder로 만들어, 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됨
private static class SingleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 해당 메서드 호출시 SingleInstanceHolder 클래스 초기화
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
- 세부 설명
- 내부 클래스를 static으로 선언하여, 싱글톤 클래스가 초기화되어도
SingleInstanceHolder
내부 클래스는 메모리에 로드되지 않음 getInstnce()
메서드를 외부에서 호출할 때,SingleInstanceHolder
내부 클래스의static 멤버를 반환
하게 되는데, 이 때 내부 클래스가 한번 초기화되면서 싱글톤 객체를 최초 생성 및 리턴- 마지막으로
final
을 지정함으로서 재할당 방지
- 내부 클래스를 static으로 선언하여, 싱글톤 클래스가 초기화되어도
- 한계점
Reflection API
,직렬화/역직렬화
를 이용하면 임의로 싱글톤 파괴할 수 있음
(7) Enum 이용
- 권장되는 두가지 방법 중 하나
enum
클래스 : 애초에 멤버를 만들때 private으로 만들고 한번만 초기화하기 때문에Thread safe
enum
내에서 상수 뿐만아니라 변수나 메서드를 선언해 사용할 수 있기 때문에 이를 활용하여 싱글톤 클래스처럼 응용이 가능
enum SingletonEnum {
INSTANCE;
private final Client dbClient;
SingletonEnum() {
dbClient = Database.getClient();
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public Client getClient() {
return dbClient;
}
}
- 장점
Reflection
을 통한 공격에도 안전
- 단점
Enum
클래스는Enum
클래스 외에는 상속이 불가능- 멀티톤 클래스로 마이그레이션 해야할 때 처음부터 코드를 다시 짜야함
5. 싱글턴 패턴의 문제점
- 모듈간의 의존성
- 하나의 인스턴스를 여러 개의 모듈에서 참조하는 경우 클래스간 결합도가 높아짐
- 테스트 어려움 :
- 생성 방식이 제한적이기 때문에 Mock으로 대체하기 어려움
- 다른 객체들과 자원을 공유하고 있기 때문에, 테스트시 항상 인스턴스 상태 초기화 필요
- 다중 서버
- 다중 서버인 경우 독립적으로 다른 메모리 공간에 있기 때문에 싱글톤 객체의 유일성을 보장받을 수 없음
6. 결론
- 싱글톤 패턴은 효율적인 자원 관리에 유용하지만, 위에서 언급한 문제점들을 충분히 고려
- 직접 구현하는 대신, 검증된 프레임워크나 라이브러리를 사용하여 싱글톤을 관리하는 것이 더 안전하고 효율적
- 예를 들어,
Spring Framework
에서는 의존성 주입(DI)을 통해 싱글톤 패턴을 안전하게 관리할 수 있으며, 멀티스레드 환경과 분산 시스템을 고려한 다양한 기능을 제공
- 예를 들어,