본문 바로가기

Java & Kotlin

[Java] 자바에서 효율적인 싱글톤 패턴 구현하기: Double-Checked Locking

싱글톤 패턴은 소프트웨어 디자인 패턴 중 하나로, 특정 클래스의 인스턴스가 하나만 생성되도록 보장하는 패턴입니다. 자바에서 싱글톤 패턴을 구현하는 다양한 방법이 있지만, 그 중에서도 Double-Checked Locking 패턴은 성능과 스레드 안전성을 모두 만족시키는 효율적인 방법입니다. 이번 글에서는 Double-Checked Locking 패턴을 중심으로 싱글톤 패턴을 구현하는 방법과 그 이유를 설명하겠습니다.

싱글톤 패턴의 기본 구현

기본적인 싱글톤 패턴은 다음과 같이 구현할 수 있습니다:

public class SingletonService {
    // 유일한 인스턴스를 저장할 정적 변수
    private static SingletonService instance;

    // private 생성자
    private SingletonService() {}

    // Singleton 인스턴스를 반환하는 메서드
    public static synchronized SingletonService getInstance() {
        if (instance == null) {
            instance = new SingletonService();
        }
        return instance;
    }
}

 

위 코드에서는 getInstance() 메서드에 synchronized 키워드를 사용하여 동기화를 처리합니다. 하지만 이 방식은 성능 저하를 유발할 수 있습니다. 매번 메서드가 호출될 때마다 동기화 비용이 발생하기 때문입니다.

Double-Checked Locking 패턴

Double-Checked Locking 패턴은 이러한 성능 저하를 해결하기 위한 방법입니다. 이 패턴은 인스턴스를 생성할 때만 동기화를 사용하고, 인스턴스가 이미 생성된 경우에는 동기화를 사용하지 않습니다. 이를 통해 불필요한 동기화를 피하면서도 스레드 안전성을 보장할 수 있습니다.

Double-Checked Locking 패턴의 구현

public class SingletonService {
    // volatile 키워드를 사용하여 instance 변수를 선언
    private static volatile SingletonService instance;

    // private 생성자
    private SingletonService() {}

    // Singleton 인스턴스를 반환하는 메서드
    public static SingletonService getInstance() {
        if (instance == null) {  // 인스턴스가 null인지 체크
            synchronized (SingletonService.class) {
                if (instance == null) {  // 인스턴스가 여전히 null인지 다시 체크
                    instance = new SingletonService();  // 인스턴스 생성
                }
            }
        }
        return instance;
    }
}

위 코드에서 중요한 부분은 instance 변수를 volatile로 선언한 것입니다. volatile 키워드는 변수의 값이 여러 스레드에 의해 변경될 수 있음을 나타내며, 모든 스레드가 해당 변수의 최신 값을 항상 읽도록 보장합니다.

Double-Checked Locking 패턴의 동작 원리

  1. 첫 번째 null 체크: getInstance() 메서드에서 먼저 instance가 null인지 확인합니다. 인스턴스가 이미 초기화된 경우, 동기화 블록을 통과하지 않고 바로 인스턴스를 반환합니다.
  2. 동기화 블록: 인스턴스가 null인 경우에만 동기화 블록에 진입합니다. 이 블록은 클래스 레벨로 동기화되어, 동시에 여러 스레드가 이 블록에 진입하는 것을 방지합니다.
  3. 두 번째 null 체크: 동기화 블록 내부에서 다시 한 번 instance가 null인지 확인합니다. 이는 다른 스레드가 이미 인스턴스를 초기화했는지 확인하기 위함입니다. 만약 여전히 null이라면, 인스턴스를 생성합니다.

왜 volatile 키워드를 사용할까?

volatile 키워드는 멀티스레드 환경에서 변수의 값을 일관되게 유지하기 위해 사용됩니다. 이 키워드는 변수의 값을 여러 스레드가 공유하고, 각 스레드가 항상 변수의 최신 값을 읽을 수 있도록 보장합니다. 이를 이해하기 위해서는 스레드와 CPU 캐시, 메인 메모리 간의 상호작용을 알아야 합니다.

쓰레드와 메모리 모델

각 스레드는 독립적으로 실행되며, CPU는 각 스레드의 작업을 빠르게 처리하기 위해 캐시를 사용합니다. 캐시는 CPU와 메인 메모리 사이에 위치한 고속 메모리로, 자주 사용되는 데이터의 복사본을 저장합니다. 하지만 이로 인해 스레드 간의 데이터 일관성 문제가 발생할 수 있습니다.

CPU 캐시와 메인 메모리

CPU 캐시는 스레드의 로컬 메모리와 비슷한 역할을 합니다. 스레드가 변수의 값을 읽고 쓸 때, 이 값은 메인 메모리가 아닌 CPU 캐시에 저장될 수 있습니다. 이는 성능을 향상시키지만, 각 스레드가 서로 다른 CPU 캐시를 사용할 경우, 변수의 값이 일관되지 않을 수 있습니다.

다음 이미지는 CPU 캐시와 메인 메모리 간의 관계를 나타냅니다

불필요한 동기화를 피하는 이유

동기화는 멀티스레드 환경에서 스레드 간의 작업을 조정하고 일관성을 유지하기 위해 필요하지만, 비용이 많이 드는 작업입니다. 불필요한 동기화를 피하는 이유는 다음과 같습니다:

  • 성능 저하 방지: 동기화 블록에 접근할 때마다 락을 획득하고 해제하는 과정에서 오버헤드가 발생합니다.
  • 스케일링 문제: 여러 스레드가 동기화 블록에 접근하려고 할 때 병목 현상이 발생할 수 있습니다.
  • 효율적인 리소스 사용: 불필요한 동기화를 피하면 CPU와 메모리 자원을 효율적으로 사용할 수 있습니다.

결론

Double-Checked Locking 패턴은 싱글톤 패턴을 구현할 때 성능과 스레드 안전성을 모두 만족시키는 효율적인 방법입니다. volatile 키워드를 사용하여 인스턴스 변수를 선언하고, 동기화 블록 내부에서 두 번의 null 체크를 통해 불필요한 동기화를 피하면서도 안전하게 싱글톤 인스턴스를 생성할 수 있습니다. 이를 통해 자바 애플리케이션의 성능을 최적화하고 안정성을 높일 수 있습니다.