Ch5. Singleton Pattern
싱글톤 패턴
1. 싱글턴 알아보기
"싱글턴 패턴은 클래스의 인스턴스가 하나만 생성되도록 제한하며, 이 인스턴스에 대한 전역적인 접근을 제공하는 디자인 패턴이다."
싱글턴 패턴(Singleton Pattern)은 객체 지향 프로그래밍에서 매우 널리 사용되는 패턴 중 하나로, 특정 클래스의 인스턴스를 하나만 생성해야 할 때 사용하는 패턴입니다. 이 패턴을 사용하면 애플리케이션 내에서 해당 클래스의 인스턴스가 오직 하나만 존재하도록 보장할 수 있습니다. 이는 시스템 내에서 공유 자원이나 공통 기능을 제공하는데 유용합니다.
핵심은 단일 인스턴스와 전역 접근입니다.
주요 장점:
전역 접근: 싱글턴 패턴을 사용하면 애플리케이션 내 어디서든 동일한 인스턴스에 접근할 수 있습니다. 이를 통해 전역적으로 상태를 관리해야 하는 경우 매우 유용합니다.
메모리 절약: 클래스의 인스턴스를 하나만 생성하기 때문에 불필요한 메모리 사용을 줄일 수 있습니다. 이는 자원을 효율적으로 관리하는 데 도움이 됩니다.
생성 비용 절감: 객체 생성 비용이 높은 경우, 싱글턴 패턴을 사용하여 인스턴스를 재사용함으로써 시스템의 성능을 향상시킬 수 있습니다.
상태 일관성: 싱글턴 패턴을 사용하면 하나의 인스턴스를 공유하므로, 상태 관리가 일관되게 유지될 수 있습니다. 이는 특히 설정 정보나 로그 관리와 같은 기능에 적합합니다.
주요 단점:
테스트 어려움: 싱글턴 패턴은 테스트하기 어려운 구조를 만들 수 있습니다. 특히, 싱글턴 인스턴스가 특정 상태를 유지하는 경우, 테스트 환경에서 이 상태를 초기화하거나 관리하는 것이 어려울 수 있습니다.
의존성 문제: 싱글턴 인스턴스에 너무 많은 기능이 의존하게 되면, 이 인스턴스가 여러 모듈에 걸쳐 종속성이 생기게 됩니다. 이는 모듈 간의 강한 결합을 초래할 수 있습니다.
멀티스레드 문제: 앞서 다뤘듯이, 싱글턴 패턴은 멀티스레딩 환경에서 주의 깊게 구현해야 합니다. 잘못 구현하면 동기화 문제로 인해 심각한 버그가 발생할 수 있습니다.
구조적 제한: 싱글턴 패턴은 클래스 설계에 제한을 가할 수 있습니다. 예를 들어, 여러 인스턴스를 생성해야 하는 경우 싱글턴 패턴이 적합하지 않을 수 있습니다.
2. 리틀 싱글턴
리틀 싱글턴은 싱글턴의 개념을 좀 더 작게 만든 변형으로, 전역적인 접근보다는 특정 범위 내에서 하나의 인스턴스만 유지하고 싶은 경우에 사용됩니다. 이를테면, 전역적으로 접근 가능한 객체가 아니라, 특정 클래스 내부나 모듈 내에서만 제한적으로 하나의 인스턴스를 유지하고 싶은 경우에 리틀 싱글턴 패턴이 적용될 수 있습니다.
싱글턴: 애플리케이션 전체에서 하나의 인스턴스만 존재하며, 어디서든 동일한 인스턴스에 접근할 수 있습니다.
리틀 싱글턴: 애플리케이션 전체가 아니라, 특정 영역(예: 클래스, 모듈)에서 하나의 인스턴스만 유지되도록 제한합니다. 전역적인 접근은 필요하지 않지만, 특정 기능 내에서 인스턴스가 공유되어야 할 때 사용됩니다.
2. 고전적인 싱글턴 패턴 구현법
싱글턴 패턴의 고전적인 구현 방법은 간단합니다. 주로 다음과 같은 단계를 따릅니다:
생성자를
private
으로 선언: 생성자를 외부에서 접근하지 못하도록 막아야 합니다. 이렇게 하면 클래스 외부에서 이 클래스를 사용해 인스턴스를 생성할 수 없습니다.자신의 타입을 인스턴스로 가지는
static
변수를 선언: 클래스 내에서 유일한 인스턴스를 참조할 수 있도록static
변수를 선언합니다.인스턴스를 반환하는
static
메서드를 작성: 인스턴스를 반환하는getInstance()
메서드를 작성하여, 인스턴스가 존재하지 않는 경우 생성하고, 이미 존재하는 경우 기존 인스턴스를 반환합니다.
3. 초콜릿 보일러 코드 살펴보기
싱글턴 패턴의 활용 예로는 초콜릿 보일러(Chocolate Boiler)와 같은 시스템을 들 수 있습니다. 초콜릿 보일러는 초콜릿과 우유를 섞어 데우는 장치로, 이 과정에서 동일한 보일러가 여러 번 생성되지 않도록 해야 합니다. 여러 개의 보일러가 존재하면, 리소스 낭비가 발생하고, 최악의 경우 보일러가 엉망이 될 수 있습니다.
4. 허쉬! 초콜릿 보일러에 문제 발생
싱글턴 패턴은 편리하고 유용하지만, 문제가 발생할 여지가 있습니다. 특히 멀티스레딩 환경에서는 여러 스레드가 동시에 getInstance()
메서드를 호출할 경우, 두 개 이상의 인스턴스가 생성될 수 있는 잠재적 위험이 있습니다.
예를 들어, 초콜릿 보일러에서 두 개의 스레드가 getInstance()
를 동시에 호출하면, 둘 다 instance
가 null
인 상태에서 인스턴스를 생성하려고 시도할 수 있습니다. 이로 인해 두 개의 보일러가 생성되어, 시스템이 엉망이 될 수 있습니다.
5. 멀티스레딩 문제 살펴보기
멀티스레딩 환경에서 싱글턴 패턴을 안전하게 구현하기 위해서는, 스레드 간의 동기화가 필수적입니다. 동기화(synchronization)를 통해 여러 스레드가 동시에 접근하는 상황을 제어해야 합니다. 이를 위해 synchronized
키워드를 사용할 수 있습니다.
6. 멀티스레딩 문제 해결하기
멀티스레딩 문제를 해결하는 또 다른 방법은 이른 초기화(Eager Initialization)와 더블 체크 잠금(Double-Checked Locking) 기법을 사용하는 것입니다.
6.1 이른 초기화(Eager Initialization)
클래스가 로드될 때 인스턴스를 미리 생성하여, 멀티스레딩 문제를 회피하는 방법입니다. 즉, 애플리케이션 시작시에 이미 인스턴스가 생성되어 준비되어 있는 방식입니다.
단점: 스레드 세이프 하지만, 애플리케이션이 실행될 때 즉시 인스턴스를 생성하기 때문에, 사용되지 않는다면 메모리 낭비가 발생할 수 있습니다. 특히, 인스턴스 생성에 큰 비용이 들거나, 해당 인스턴스가 애플리케이션의 일부 흐름에서만 필요할 때는 비효율적일 수 있습니다.
6.2 더블 체크 잠금(Double-Checked Locking)
처음 getInstance()
호출 시에만 동기화를 적용하고, 이후에는 동기화를 피하는 방법입니다.
7. 더 효율적인 멀티스레딩 문제 해결하기
이른 초기화와 더블 체크 잠금 외에도, 다양한 최적화 기법이 존재합니다. 그 중 하나는 내부 클래스(Inner Class)를 이용하는 방법입니다. 내부 클래스는 클래스가 로드될 때 초기화되지 않고, getInstance()
가 호출될 때 비로소 인스턴스가 생성됩니다.
8. 싱글턴 패턴의 변형
멀티톤 패턴(Multiton Pattern): 싱글턴 패턴의 변형으로, 여러 개의 인스턴스를 관리하지만 인스턴스의 수는 미리 정의된 제한된 개수로 제한됩니다.
레이지 싱글턴(Lazy Singleton): 초기화 비용이 높은 경우, 인스턴스를 미리 생성하지 않고 처음 사용할 때 생성하는 패턴입니다. 더블 체크 잠금이나 이른 초기화가 이러한 패턴에 해당합니다.
레지스트리 싱글턴(Registry Singleton): 싱글턴 인스턴스를 레지스트리에 저장하고, 필요한 경우 레지스트리에서 해당 인스턴스를 가져오는 방식입니다.
9. 질문
9.1 모든 메서드와 변수를 static으로 선언해서 클래스를 만들면 되지 않나요? 결과적으로 싱글턴 패턴을 사용하는 것과 똑같을 것 같은데요?
모든 메서드와 변수를 static
으로 선언한 클래스는 싱글턴 패턴과 유사해 보일 수 있지만, 두 가지 중요한 차이가 있습니다.
첫째, static
으로 선언된 클래스는 인스턴스화가 필요하지 않으며, 전역 상태를 가진 객체가 없기 때문에 상속이나 다형성을 활용할 수 없습니다. 반면, 싱글턴 클래스는 특정 객체의 인스턴스가 하나만 존재한다는 점에서 다른 객체지향적 설계를 유지할 수 있습니다.
둘째, static
클래스는 언제든지 메모리에 로드될 수 있어 **지연 초기화(lazy initialization)**가 불가능합니다. 싱글턴 패턴은 인스턴스를 실제로 필요할 때 생성할 수 있어, 리소스를 효율적으로 관리할 수 있습니다.
9.2 클래스 로더와 관련된 문제는 없나요? 클래스 로더가 각각 다른 싱글턴의 인스턴스를 가질 수 있다는 얘기를 들었거든요.
네, 클래스 로더와 관련된 문제가 있을 수 있습니다. 자바에서 클래스 로더는 JVM이 클래스를 메모리에 로드하는 역할을 합니다.
만약 애플리케이션에 여러 개의 클래스 로더가 존재한다면, 각 클래스 로더는 동일한 클래스를 별도로 로드하여 서로 다른 인스턴스를 생성할 수 있습니다. 이로 인해, 싱글턴 패턴의 본래 목적이 무색해질 수 있습니다. 특히, 애플리케이션 서버와 같은 환경에서 클래스 로더가 여러 개일 경우, 의도치 않게 여러 개의 싱글턴 인스턴스가 생성될 수 있습니다. 이 문제를 해결하기 위해서는 정적 팩토리 메서드나 서비스 로더를 사용해 싱글턴 인스턴스를 관리하는 방법을 고려할 수 있습니다.
9.3 리플렉션, 직렬화, 역직렬화 문제도 있지 않나요?
리플렉션 문제: 리플렉션을 사용하면 클래스의
private
생성자에 접근할 수 있기 때문에, 싱글턴 클래스의 인스턴스를 여러 개 생성할 수 있는 위험이 있습니다. 이를 방지하려면, 싱글턴 클래스의 생성자에서 리플렉션을 통한 새로운 인스턴스 생성 시도에 대해 예외를 발생시키는 방법이 필요합니다.private Singleton() { if (instance != null) { throw new IllegalStateException("이미 인스턴스가 존재합니다."); } }
직렬화 및 역직렬화 문제: 직렬화와 역직렬화는 객체를 바이트 스트림으로 변환하고, 다시 객체로 복원하는 과정입니다. 싱글턴 클래스가 직렬화된 후 역직렬화되면 새로운 인스턴스가 생성되어 싱글턴 원칙이 깨질 수 있습니다. 이를 방지하기 위해서는
readResolve()
메서드를 오버라이드하여, 역직렬화 시 기존의 싱글턴 인스턴스를 반환하도록 해야 합니다.private Object readResolve() { return getInstance(); }
9.4 싱글턴은 느슨한 결합 원칙에 위배되지 않나요? Singleton에 의존하는 객체는 전부 하나의 객체에만 결합된 것 아닌가요?
싱글턴 패턴은 종종 느슨한 결합 원칙에 위배될 수 있다는 지적을 받습니다. 이는 싱글턴 인스턴스에 의존하는 객체들이 모두 동일한 인스턴스에 강하게 결합되기 때문입니다. 이런 결합은 객체 지향 설계에서 유연성을 저하시킬 수 있습니다. 예를 들어, 테스트나 확장성을 고려할 때 문제가 될 수 있습니다. 이를 해결하기 위해서는 싱글턴 객체를 인터페이스로 추상화하거나, 의존성 주입(Dependency Injection)을 통해 결합도를 낮출 수 있습니다. 이렇게 하면, 실제 인스턴스를 테스트 목적으로 **모의 객체(Mock Object)**로 대체할 수 있어 테스트 용이성을 높일 수 있습니다.
9.5 싱글턴이 서브클래스를 만들어도 되는 건가요?
싱글턴 클래스의 서브클래스를 만드는 것은 일반적으로 권장되지 않습니다. 싱글턴의 목적은 하나의 인스턴스만 존재하도록 보장하는 것이기 때문에, 서브클래스를 생성하면 이러한 제약이 깨질 수 있습니다. 특히, 서브클래스가 추가로 인스턴스를 만들 수 있는 상황에서는 싱글턴의 의미가 무색해집니다. 만약 서브클래스를 반드시 만들어야 한다면, 서브클래스의 생성자를 protected
로 선언하고, 서브클래스도 싱글턴으로 구현하여 상속받은 클래스들 역시 하나의 인스턴스만 가지도록 해야 합니다. 그러나, 이런 구조는 복잡성을 증가시킬 수 있으므로 가능하면 피하는 것이 좋습니다.
Last updated