// ❌ 나쁜 예: 여러 인스턴스가 생성될 수 있는 코드
public class DatabaseConnection {
public DatabaseConnection() {
// 데이터베이스 연결 설정
}
}
// 클라이언트 코드에서
DatabaseConnection conn1 = new DatabaseConnection();
DatabaseConnection conn2 = new DatabaseConnection();
DatabaseConnection conn3 = new DatabaseConnection();
// → 불필요한 여러 개의 연결이 생성됨!
문제점:
private Singleton() {} // 외부에서 new Singleton() 불가능!
왜 Private인가?
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
왜 Static인가?
Singleton.getInstance()로 클래스 이름으로 직접 호출private static Singleton uniqueInstance;
Singleton
─────────────────
-uniqueInstance (static)
-Singleton() (private constructor)
+getInstance() (static)
핵심 원칙:
public class Singleton {
// 1. 유일한 인스턴스를 저장할 static 변수
private static Singleton uniqueInstance;
// 2. private constructor로 외부 생성 차단
private Singleton() {
// 초기화 코드
}
// 3. 인스턴스 접근을 위한 public static 메서드
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// 기타 유용한 메서드들
public void doSomething() {
System.out.println("Singleton 작업 수행");
}
}
public class Client {
public static void main(String[] args) {
// ❌ 컴파일 에러: constructor가 private
// Singleton s = new Singleton();
// ✅ 올바른 사용법
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
// 같은 인스턴스인지 확인
System.out.println(s1 == s2); // true
s1.doSomething();
}
}
public static Singleton getInstance() {
if (uniqueInstance == null) { // Thread 1, 2 모두 null 체크
uniqueInstance = new Singleton(); // 두 개의 인스턴스 생성!
}
return uniqueInstance;
}
타이밍 문제:
Thread 1 Thread 2 uniqueInstance
────────────────────────────────────────────────────────────────
if (null) ✓ null
if (null) ✓ null
new Singleton() → 생성 <object1>
new Singleton() → 생성 <object2> ⚠️ 문제!
return <object1>
return <object2>
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
// synchronized로 한 번에 한 스레드만 접근
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
장점: 구현이 간단하고 확실히 동작
단점: 매번 접근할 때마다 동기화 오버헤드 발생
public class Singleton {
// 클래스 로딩 시점에 인스턴스 생성
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
// 이미 생성된 인스턴스 반환만 하면 됨
public static Singleton getInstance() {
return uniqueInstance;
}
}
장점:
단점:
public class Singleton {
// volatile: 메모리 가시성 보장
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) { // 첫 번째 체크 (동기화 X)
synchronized(Singleton.class) { // 동기화 블록
if (uniqueInstance == null) { // 두 번째 체크 (동기화 O)
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
동작 원리:
1. 첫 번째 null 체크: 동기화 없이 빠르게 확인
2. null이면 동기화 블록 진입 (다른 스레드 대기)
3. 두 번째 null 체크: 대기 중 다른 스레드가 생성했을 수 있음
4. 여전히 null이면 생성
왜 volatile이 필요한가?
Thread 1 Main Memory Thread 2
[작업 메모리] [공유 메모리] [작업 메모리]
x = 100 ────→ x = ? ←──── x = ?
volatile 없이:
class Something {
private int x = 0; // 각 스레드가 캐시에 복사본 유지 가능
private int y = 0;
public void write() {
x = 100;
y = 50;
}
public void read() {
if (x < y) { // ⚠️ x=0, y=50을 볼 수 있음!
System.out.println("x < y");
}
}
}
volatile 사용:
class Something {
private volatile int x = 0; // 항상 메인 메모리에서 읽고 씀
private volatile int y = 0;
// 이제 안전!
}
| 특성 | synchronized | volatile |
|---|---|---|
| 적용 대상 | 메서드/블록 | 변수 |
| 원자성 보장 | O | X |
| 가시성 보장 | O | O |
| 성능 | 상대적으로 느림 | 빠름 |
| 사용 시기 | 복잡한 연산 | 단순 읽기/쓰기 |
public class ThreadPoolManager {
private volatile static ThreadPoolManager instance;
private ExecutorService executor;
private ThreadPoolManager() {
// 스레드풀 초기화 (무거운 작업)
executor = Executors.newFixedThreadPool(10);
System.out.println("스레드풀 초기화 완료");
}
public static ThreadPoolManager getInstance() {
if (instance == null) {
synchronized(ThreadPoolManager.class) {
if (instance == null) {
instance = new ThreadPoolManager();
}
}
}
return instance;
}
public void executeTask(Runnable task) {
executor.execute(task);
}
public void shutdown() {
executor.shutdown();
}
}
// 사용 예시
public class Application {
public static void main(String[] args) {
// 여러 곳에서 호출해도 하나의 스레드풀만 사용
ThreadPoolManager pool = ThreadPoolManager.getInstance();
pool.executeTask(() -> System.out.println("작업 1"));
pool.executeTask(() -> System.out.println("작업 2"));
pool.shutdown();
}
}
| 요소 | 역할 | 왜 이렇게? |
|---|---|---|
| Private Constructor | 외부 생성 차단 | 인스턴스 개수 통제 |
| Static Instance | 유일한 인스턴스 보관 | 클래스 레벨에서 공유 |
| Static getInstance() | 전역 접근 지점 | 인스턴스 없이도 호출 가능 |
| Lazy/Eager | 생성 시점 선택 | 필요에 따라 최적화 |
상황에 따른 선택:
1. 간단한 애플리케이션 → Eager Initialization
2. 성능이 중요하지 않음 → synchronized 메서드
3. 고성능 + 멀티스레드 → Double-Checked Locking (Java 5+)
4. 최신 Java → Enum Singleton (가장 안전)
// Joshua Bloch가 추천하는 방법 (Effective Java)
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Enum Singleton!");
}
}
// 사용
Singleton.INSTANCE.doSomething();
장점: