목적
- 단 하나의 유일한 객체를 만들기 위한 디자인 패턴
- 클래스의 인스턴스를 오직 하나만 만들어서 글로벌하게 접근할 수 있도록 한다.
- 메모리 절약을 위해 인스턴스가 필요할 때 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 가져와 활용하는 기법
- 대부분 리소스를 많이 차지하는 역할을 하는 무거운 클래스를 대상으로 한다.
- ex) 데이터베이스 연결 모듈, 디스크 연결, 네트워크 통신, DBCP 커넥션풀, 스레드풀, 캐시, 로그 기록 객체
자바로 싱글톤 패턴 구현
생성자를 private으로 설정해서 외부에서 인스턴스로 만들 수 없게 만드는 것이 중요하다.
1. Lazy initialization
public class Singleton {
private static Singleton instance;
private Singleton();
public static Singleton getInstance() {
if (instance == null) {
intance = new Singleton();
}
return instance;
}
}
- 메서드를 호출했을 때 인스턴스 변수의 초기화 여부에 따라 초기화하거나 이미 존재하는 것을 반환한다.
- 사용하는 시점에 생성할 수 있는 장점이 있다.
- 멀티 쓰레드 환경에서 안전하지 않다. (thread-safe X)
2. Eager initialization
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton();
public static Singleton getInstance() {
return instance;
}
}
- 가장 직관적인 방법이다.
- static final로 선언하여, Singleton 클래스의 유일한 인스턴스를 저장하고, immutable 하다.
- static은 당장 사용하지 않는 객체더라도, 클래스 로딩 시점에 정적 필드 instance가 초기화된다.
- 만일 리소스가 큰 객체일 경우 공간 자원 낭비가 발생한다.
- 생성자 생성의 비용이 크지 않더라면, 해당 방법을 사용하더라도 큰 무리는 없다.
3. Thread safe initialization
public class Singleton {
private static Singleton instance;
private Singleton();
public static synchronized Singleton getInstance() {
if (instance == null) {
intance = new Singleton();
}
return instance;
}
}
- synchronized 키워드를 통해 하나의 스레드만 해당 메서드에 접근할 수 있도록 한다.
- 따라서 멀티 쓰레드 환경에서도 안전하다. (thread-safe O)
- 여러 개의 스레드가 인스턴스를 가져올 때마다 synchronized 메서드를 매번 호출하여 동기화 작업이 일어나 성능이 떨어진다.
- 인스턴스가 초기화되지 않은 상태에서는 동기화 작업을 통해 인스턴스가 단 하나만 만들어지는 것을 보장할 수 있지만,
- 이미 기존의 인스턴스가 만들어진 상황에서도 동기화 작업이 일어난다.
4. Double-Checked Locking
public class Singleton {
private static volatile Singleton instance;
private Singleton();
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
intance = new Singleton();
}
}
}
return instance;
}
}
- 이미 인스턴스가 기존에 존재하는 경우 synchronized의 동기화 과정이 일어나지 않는다.
- 인스턴스가 없을 경우, 최초 초기화할 때만 동기화 과정이 일어난다.
- volatile 키워드를 통해 변수의 일관성과 가시성을 보장한다.
- 변수를 읽고 쓰는 작업이 캐시 메모리가 아닌 메인 메모리에서 이루어지도록 한다. 따라서 변경된 값을 다른 스레드가 즉시 알 수 있어 가시성(visibility) 문제를 해결한다.
- volatile 은 JVM 1.5 이상이어야 한다.
- volatile 키워드는 모든 스레드 간의 상호작용을 동기화하는 완전한 동기화 메커니즘이 아니다.
- volatile은 변수의 가시성과 일관성을 보장하지만, 스레드 간의 상호작용이 여러 단계로 이루어지거나 원자적이지 않은 연산을 수행하는 경우에는 동기화 메커니즘(synchronized 블록, Lock)을 추가해야 한다.
- 따라서 volatile을 사용하는 것을 지양하는 편이다.
5. LazyHolder 👍🏻
public class Singleton {
private Singleton();
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 정적 내부 클래스 SingletonHolder는 Singleton의 인스턴스를 지연 초기화하는 역할을 한다.
- getInstance() 메서드가 호출되면 SingletonHolder.INSTANCE를 통해 Singleton 인스턴스가 초기화되고 반환됩니다.
- 이를 통해 지연 초기화와 동시에 스레드 안전성이 보장됩니다.
- 다만 클라이언트가 임의로 Reflection API, 직렬화/역직렬화를 통해 싱글톤을 파괴할 수 있다는 단점을 가지고 있다.
- 안정성보다는 성능이 중요시되는 환경에서 사용하기 적합하다.
6. Enum 👍🏻
public enum Settings {
INSTANCE;
}
Settings settings = Settings.INSTANCE;
- enum은 멤버를 만들 때, private로 만들고 한 번만 초기화한다.
- 따라서 유일한 인스턴스를 만들고, 동시성을 보장한다.
- 클라이언트에서 Reflection을 통한 공격에도 안전하다.
- 하지만 클래스 상속이 필요할 때, enum 외의 클래스 상속은 불가능하다.
- 직렬화와 안정성이 중요시하는 환경에서 사용하기 적합하다.
싱글톤 구현 방법을 깨트리는 방법
1. 리플렉션
Singleton settings = new Singleton();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton sigleton = constructor.newInstance();
settings == settings1 // false
- 리플렉션을 사용하여, Singleton 클래스의 private 접근자를 무시하고 생성자에 접근할 수 있다.
- newInstance()를 통해 새로운 객체를 동적으로 생성할 수 있다.
- Singleton 패턴의 목적을 깰 수 있다.
2. 직렬화 & 역직렬화
public calss Singleton implements Serializable {
...
}
Singleton settings = new Singleton();
- Singleton 클래스를 직렬화하고 역직렬화하는 경우, 역직렬화된 객체는 새로운 인스턴스가 된다.
- 직렬화/역직렬화 과정에서 생성자는 호출되지 않기 때문에, Singleton 클래스의 인스턴스 생성 방지 메커니즘이 우회될 수 있다.
- Singleton 패턴의 목적을 깰 수 있다.
- Singleton 클래스를 직렬화할 때 Singleton 패턴을 유지하기 위해서는 readResolve() 메서드를 정의해야 합니다.
- readResolve() 메서드는 역직렬화된 객체를 반환하는 역할을 합니다. readResolve() 메서드를 구현하여 항상 동일한 Singleton 인스턴스를 반환하도록 지정할 수 있습니다.
- readResolve() 메서드는 역직렬화된 객체를 반환하는 역할을 합니다. readResolve() 메서드를 구현하여 항상 동일한 Singleton 인스턴스를 반환하도록 지정할 수 있습니다.
public class Singleton implements Serializable {
...
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
protected Object readResolve() {
return instance;
}
}
싱글톤의 문제점
- 모듈 간 의존성이 높아진다.
- 싱글톤은 대부분 인터페이스가 아닌 클래스의 객체를 미리 생성하고 정적 메서드를 사용하여 다른 클래스에서 사용하기 때문에 클래스 간의 강한 의존성과 높은 결합이 생기게 된다.
- 싱글톤 인스턴스가 변경되면 이를 참조하고 있는 모든 모듈들도 수정이 필요할 수 있다.
- SOLID 위배 사례가 많다.
- 싱글톤 인스턴스 자체가 하나만 생성하기 때문에 여러 가지 책임을 지닐 수 있다. (SRP 위반)
- 싱글톤 클래스를 많은 클래스가 참조한다면, 클래스들 간의 결합도가 높아질 수 있다. (OCP 위반)
- 의존 관계상 클라이언트가 인터페이스와 같은 추상화가 아닌, 구체 클래스에 의존하게 된다. (DIP도 위반)
- 단위 테스트의 어려움
- 싱글톤 클래스를 사용하는 모듈을 테스트하기 어렵다.
- 단위 테스트는 테스트가 서로 독립적이어야 하고 테스트를 어떤 순서로든 실행할 수 있어야 하는데, 싱글톤 인스턴스는 자원을 공유하고 있기 때문에, 테스트가 결함 없이 수행되려면 매번 인스턴스의 상태를 초기화시켜주어야 한다.
- 싱글톤은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기 어려우며, 동적으로 객체를 주입하기도 힘들다.
스프링 컨테이너에서 객체 관리
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 Ioc 방식의 컨테이너에서 싱글톤으로 관리한다.
- private 생성자를 사용할 필요가 없어, 상속을 통해 싱글톤 객체를 확장하거나 사용할 수 있는 가능성을 열어둔다.
- 테스트 환경에서 동일한 인스턴스를 공유하여 일관된 테스트를 수행할 수 있고, 필요한 경우 Mock 객체로 대체하여 테스트할 수 있다.
- 스프링 컨테이너에 객체가 빈 등록되면, 컨테이너가 해당 빈을 싱글톤으로 생성하고 관리하기 때문에 1개의 객체 생성을 보장받을 수 있다.
- 스프링 컨테이너를 사용하여 객체를 싱글톤으로 관리하면 객체지향적인 개발을 할 수 있다. DI을 통해 객체 간의 관계를 구성하고, 인터페이스를 통해 프로그래밍할 수 있습니다.