Singleton Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
8/19

1. 왜 Singleton Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 여러 인스턴스가 생성될 수 있는 코드
public class DatabaseConnection {
    public DatabaseConnection() {
        // 데이터베이스 연결 설정
    }
}

// 클라이언트 코드에서
DatabaseConnection conn1 = new DatabaseConnection();
DatabaseConnection conn2 = new DatabaseConnection();
DatabaseConnection conn3 = new DatabaseConnection();
// → 불필요한 여러 개의 연결이 생성됨!

문제점:

  • 시스템 리소스가 낭비됨 (프린터 스풀러, 윈도우 매니저, 로그 등)
  • 전역 상태 관리가 어려움
  • 여러 인스턴스 간 데이터 불일치 발생 가능
  • new 연산자로 누구나 인스턴스를 생성할 수 있음

2. Singleton의 핵심 설계 요소 (시험)

1. Private Constructor

  • "외부에서 함부로 인스턴스를 만들 수 없게"
  • 생성자를 private으로 선언하여 new 연산자 사용 차단
private Singleton() {}  // 외부에서 new Singleton() 불가능!

왜 Private인가?

  • public이면 누구나 new로 인스턴스 생성 가능
  • 단 하나의 인스턴스만 보장하기 위해 필요

2. Static getInstance() 메서드

  • "인스턴스를 얻는 유일한 방법"
  • 전역 접근 지점 제공
public static Singleton getInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

왜 Static인가?

  • 인스턴스가 없는 상태에서도 호출할 수 있어야 함
  • Singleton.getInstance()로 클래스 이름으로 직접 호출

3. Static Instance 변수

  • "생성된 인스턴스를 기억"
  • 한 번 생성된 인스턴스를 계속 재사용
private static Singleton uniqueInstance;

3. Singleton Pattern 기본 구조

        Singleton
    ─────────────────
    -uniqueInstance  (static)
    
    -Singleton()     (private constructor)
    +getInstance()   (static)

핵심 원칙:

  • 클래스가 자신의 유일한 인스턴스를 관리
  • 외부에서는 getInstance()를 통해서만 접근 가능
  • Lazy Initialization: 필요할 때 생성

4. 단일 스레드 환경 구현

기본 구현 (Single-Thread)

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();
    }
}

5. 멀티스레드 환경의 문제

문제 상황

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>

6. 멀티스레드 해결 방법

방법 1: synchronized 메서드 (가장 간단)

public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    // synchronized로 한 번에 한 스레드만 접근
    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

장점: 구현이 간단하고 확실히 동작
단점: 매번 접근할 때마다 동기화 오버헤드 발생


방법 2: Eager Initialization (즉시 생성)

public class Singleton {
    // 클래스 로딩 시점에 인스턴스 생성
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    // 이미 생성된 인스턴스 반환만 하면 됨
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

장점:

  • 스레드 안전 (JVM이 클래스 로딩 시 보장)
  • 동기화 오버헤드 없음

단점:

  • 사용하지 않아도 무조건 생성됨
  • 전역 변수와 유사한 단점

방법 3: Double-Checked Locking (DCL) ⭐ 권장

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이 필요한가?

  • Java 메모리 모델에서 객체 생성은 3단계:
    1. 메모리 할당
    2. 객체 초기화
    3. 변수에 할당
  • volatile 없으면 2와 3의 순서가 바뀔 수 있음 (reordering)
  • 다른 스레드가 초기화되지 않은 객체를 볼 수 있음!

7. volatile 키워드 이해하기

Working Memory vs Main Memory

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 vs volatile 비교

특성synchronizedvolatile
적용 대상메서드/블록변수
원자성 보장OX
가시성 보장OO
성능상대적으로 느림빠름
사용 시기복잡한 연산단순 읽기/쓰기

8. 실전 예제: 스레드풀 관리자

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();
    }
}

9. 핵심 정리

Singleton Pattern의 구성

요소역할왜 이렇게?
Private Constructor외부 생성 차단인스턴스 개수 통제
Static Instance유일한 인스턴스 보관클래스 레벨에서 공유
Static getInstance()전역 접근 지점인스턴스 없이도 호출 가능
Lazy/Eager생성 시점 선택필요에 따라 최적화

언제 사용하는가?

  • 리소스 관리: 데이터베이스 연결, 파일 시스템, 프린터 스풀러
  • 전역 설정: 환경 설정, 캐시 관리자, 로거
  • 팩토리 객체: 객체 생성을 담당하는 팩토리
  • 상태 관리: 애플리케이션 전역 상태

주의사항

  • ⚠️ 멀티스레드 환경: 반드시 동기화 고려
  • ⚠️ 테스트 어려움: 전역 상태로 인한 테스트 격리 문제
  • ⚠️ 과도한 사용 금지: 남용하면 전역 변수처럼 됨

구현 방법 선택 가이드

상황에 따른 선택:

1. 간단한 애플리케이션 → Eager Initialization
2. 성능이 중요하지 않음 → synchronized 메서드  
3. 고성능 + 멀티스레드 → Double-Checked Locking (Java 5+)
4. 최신 Java → Enum Singleton (가장 안전)

관련 패턴

  • Factory Pattern: Singleton으로 구현 가능
  • Facade Pattern: 종종 Singleton으로 구현
  • State Pattern: State 객체를 Singleton으로 관리

10. 보너스: Enum Singleton (Java 최고의 방법)

// Joshua Bloch가 추천하는 방법 (Effective Java)
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("Enum Singleton!");
    }
}

// 사용
Singleton.INSTANCE.doSomething();

장점:

  • 스레드 안전 (JVM이 보장)
  • Serialization 안전
  • Reflection 공격 방어
  • 코드가 가장 간결
profile
다들 응원합니다.

0개의 댓글