싱글톤 패턴
포스트
취소

싱글톤 패턴

싱글톤 패턴은 객체지향 프로그래밍 패턴 중 하나로, 클래스의 인스턴스가 하나만 생성되도록 보장하고, 그 인스턴스에 접근할 수 있는 전역적인 접근점을 제공합니다.

장점

  1. 메모리 효율: 인스턴스를 한 번만 생성하므로 메모리 사용을 최소화할 수 있습니다.
  2. 공유 리소스: 데이터베이스 연결이나 네트워크 소켓과 같은 공유 리소스에 대한 동일한 인스턴스 접근이 가능합니다.
  3. 인스턴스 제어: 싱글톤 패턴을 사용하면 인스턴스의 생성과 생명주기를 제어할 수 있습니다. 따라서 초기화나 종료 시 필요한 특별한 동작을 수행하기 좋습니다.

단점

  1. 테스트하기 어려움: 싱글톤 패턴을 사용하면 테스트하기 어려울 수 있습니다. 테스트를 수행할 때, 각각의 테스트 케이스는 독립적이어야 하는데, 싱글톤 객체는 상태를 공유하게 됩니다. 따라서 한 테스트에서의 변경이 다른 테스트에 영향을 줄 수 있어, 상태의 격리가 어려워집니다. 또한, 특정 테스트 케이스에서 싱글톤 인스턴스의 특정 상태나 동작을 모방하기 위해 Mocking하는 것도 복잡해질 수 있습니다.
  2. 글로벌 상태: 싱글톤 패턴은 전역 상태를 만듭니다. 전역 상태는 코드의 예측가능성과 읽기 쉬움을 해칠 수 있으며, 다른 코드와의 간섭 가능성이 있습니다.
  3. 확장성 문제: 싱글톤 클래스를 상속하는 것은 복잡할 수 있습니다. 하위 클래스도 싱글톤으로 작동하게 할 지, 혹은 다중 인스턴스를 허용할 지 결정해야 합니다.
  4. 스레드 안전성: 싱글톤 인스턴스의 생성 과정이 스레드에 안전하지 않을 수 있습니다. 이를 해결하기 위한 다양한 방법이 있지만, 각각의 방법이 추가적인 리소스를 사용하거나 복잡도를 증가시킬 수 있습니다.

자바에서의 싱글톤 패턴 구현

싱글톤 패턴을 구현하는 방법은 여러 가지가 있지만, 가장 일반적으로 사용되는 방법과 효율적인 방법 몇 가지를 소개하겠습니다.

기본적인 구현

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {} // private constructor prevents instantiation from other classes

    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) { // Lazy Initialization
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

참고: 위의 예제는 synchronized 키워드를 사용하여 스레드 안전성을 보장합니다. 하지만 이 방식은 비교적 성능 저하가 발생할 수 있습니다.

Double-Checked Locking

Double-Checked Locking을 활용하면, 인스턴스가 이미 생성된 후의 getInstance() 호출에서 동기화를 피할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonDoubleCheckedLocking {
    private volatile static SingletonDoubleCheckedLocking uniqueInstance;

    private SingletonDoubleCheckedLocking() {}

    public static SingletonDoubleCheckedLocking getInstance() {
        if (uniqueInstance == null) {
            synchronized (SingletonDoubleCheckedLocking.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new SingletonDoubleCheckedLocking();
                }
            }
        }
        return uniqueInstance;
    }
}

참고: volatile 키워드는 멀티스레드 환경에서 uniqueInstance 변수의 작업이 올바른 순서로 수행되도록 합니다.

Bill Pugh Singleton Implementation

Bill Pugh 방법은 JVM의 클래스 로더 메커니즘과 클래스의 로딩 시점을 이용하여 초기화 문제를 해결합니다.

1
2
3
4
5
6
7
8
9
10
11
public class SingletonBillPugh {
    private SingletonBillPugh() {}

    private static class SingletonHelper {
        private static final SingletonBillPugh INSTANCE = new SingletonBillPugh();
    }

    public static SingletonBillPugh getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

이 방식에서는 내부 정적 도우미 클래스를 사용하여 싱글톤 인스턴스를 유지합니다. getInstance() 메서드는 호출될 때 도우미 클래스를 로드하며, 도우미 클래스는 인스턴스를 한 번만 생성합니다. JVM에서 클래스 로딩은 스레드 안전하므로, 추가적인 동기화가 필요 없습니다.

각 방식에는 특징과 장점이 있으므로, 개발 환경과 요구 사항에 따라 적합한 방법을 선택하는 것이 중요합니다.

싱글톤 패턴의 주의점

  1. 멀티스레딩 환경: 싱글톤 인스턴스가 아직 생성되지 않았을 때, 여러 스레드가 동시에 getInstance() 메서드를 호출하면 여러 개의 인스턴스가 생성될 수 있습니다. 이를 방지하기 위해서는 스레드 안전성을 보장하는 방식, 예를 들어 synchronized 키워드를 사용하여 동기화하는 등의 방법을 사용해야 합니다.

  2. Lazy Initialization vs Eager Initialization: 싱글톤 패턴에는 주로 두 가지 초기화 방식이 사용됩니다. “Lazy Initialization”은 실제로 객체가 필요할 때까지 인스턴스를 생성하지 않는 방식입니다. 반면, “Eager Initialization”은 애플리케이션 시작 시점에 미리 인스턴스를 생성하는 방식입니다. 용도나 환경에 따라 적절한 초기화 방식을 선택하는 것이 중요합니다.

  3. 직렬화(Serialization): 싱글톤 패턴을 사용하는 객체를 직렬화하고 역직렬화할 때 주의가 필요합니다. 표준 직렬화 과정을 거친 후 객체를 역직렬화하면 새로운 인스턴스가 생성될 수 있습니다. 이로 인해 싱글톤의 원칙이 깨질 수 있습니다. 이 문제를 해결하기 위해서는 readResolve() 메서드를 구현하여 역직렬화 시 항상 동일한 싱글톤 인스턴스를 반환하도록 해야 합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    class Singleton implements Serializable {
        private static final Singleton instance = new Singleton();
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            return instance;
        }
    
        private Object readResolve() {
            // 역직렬화될 때 싱글톤 인스턴스를 반환
            return instance;
        }
    }
    

스프링에서의 싱글톤 패턴

  1. 빈(Bean)의 스코프(Scope): 스프링에서 객체(빈)의 생성과 관리 방법을 정의하는 것을 스코프라고 합니다. 기본적으로 스프링은 빈을 싱글톤 스코프로 생성하므로, 스프링 컨테이너(ApplicationContext) 내에서는 해당 타입의 빈 객체를 하나만 갖게 됩니다.

  2. 의존성 주입: 스프링은 의존성 주입(Dependency Injection, DI)을 통해 객체간의 의존성을 관리합니다. 싱글톤 빈에 의존성을 주입할 때, 해당 빈이 싱글톤 스코프를 갖는다면 항상 같은 인스턴스가 주입됩니다.

예제

스프링의 @Component 또는 @Service, @Repository, @Controller 등의 어노테이션을 사용해 클래스를 정의하면, 해당 클래스의 인스턴스는 기본적으로 싱글톤 스코프로 관리됩니다.

1
2
3
4
@Service
public class ExampleService {
    // ...
}

위의 ExampleService는 스프링 컨테이너에서 기본적으로 싱글톤으로 관리됩니다.

또한, @Autowired나 생성자 주입을 사용하여 의존성을 주입하면, 싱글톤 스코프의 빈은 항상 동일한 인스턴스가 주입됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class ExampleController {

    private final ExampleService exampleService;

    @Autowired
    public ExampleController(ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    // ...
}

여기서 ExampleControllerExampleService에 의존하며, ExampleService의 싱글톤 인스턴스가 주입됩니다.

요약

스프링 프레임워크에서는 싱글톤 패턴의 개념을 내부적으로 활용하여 빈의 생명주기와 스코프를 관리하며, 이를 통해 개발자는 복잡한 객체 생명주기와 인스턴스 관리를 스프링에 맡기고 비즈니스 로직에만 집중할 수 있게 됩니다.

결론

싱글톤 패턴은 특정 클래스의 인스턴스가 하나만 생성되는 것을 보장하면서도 그 인스턴스에 전역적으로 접근할 수 있는 방법을 제공합니다. 그러나 사용 시 이 글에서 언급한 단점을 꼭 고려하여 적절한 상황에 사용해야 합니다. 또한, 싱글톤 패턴은 스프링 프레임워크와 같은 대규모 애플리케이션에서 리소스의 효율적인 관리를 위해 매우 유용하게 사용되고 있습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.