싱글톤(Singleton)패턴: 생성패턴
인스턴스가 오직 하나만 생성되는 것을 보장하고 어디서든 동일한 인스턴스에 접근할 수 있도록 하는 디자인 패턴
커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 경우 인스턴스를 여러 개 만들게 되면 불필요한 자원을 사용하게 되고, 또 프로그램이 예상치 못한 결과를 낳을 수 있다. 싱글턴 패턴은 오직 인스턴스를 하나만 만들고 그것을 계속해서 재사용한다. 여러 문제가 있기 때문에 주로 공유하는 경우에 사용한다.
그냥 static 키워드를 붙여 전역상태로 만들어 사용하면 되지 않나요??
싱글톤 패턴은 클래스 자신이 자기의 인스턴스에 접근하는 방법을 따로 관리하므로 static 키워드를 붙여 따로 전역변수를 생성하는 것보다 좋은 방법이다.
자기 자신의 생성자를 private로 접근 제어하고, 클래스 내부에서 객체를 생성한 후, 외부에서 접근하는 방법을 클래스가 제공한다.
싱글톤 패턴은 주로 데이터베이스에서 커넥션 풀, 슬레드풀, 캐시, 로그 기록 객체 등에 사용된다.
싱글톤 패턴을 사용할 때 주의해야 할 점이 있다.
싱글톤 객체는 상태 정보를 내부에 갖고 있지 않은 무상태(stateless)방식으로 만들어져야 한다. 동시에 싱글톤 오브젝트의 변수를 수정하는 것은 매우 위험한 일이다. 저장할 공간이 하나 뿐이니, 서로 값을 수정하면, 자신이 저장하지 않은 값을 읽어올 수도 있기 때문이다. 따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지(stateful) 방식으로 만들지 않는다.
싱글톤의 장단점
장점
- 고정된 메모리 영역을 사용하기 때문에 해당 객체에 접근할 때, 메모리 낭비를 방지할 수 있다.
- 이미 생성된 인스턴스를 활용하니 속도 측면에서도 이점이 있다.
- 정적으로 만들어지기 때문에 데이터 공유가 쉽다
단점
- 여러 인스턴스가 싱글톤 인스턴스의 데이터에 동시에 접근하게 되면, 동시성 문제가 발생할 수 있다.
- 자원을 공유하기 때문에 테스트 수행이 어렵다.(목 오브젝트로 대체하기 힘들다)
- 테스트 시 초기화 과정에서 동적으로 주입하기 힘들다.
- 클라이언트가 구체 클래스에 의존하게 된다.(DIP, OCP 위반할 가능성이 높음)
- 자식클래스를 만들 수 없다, 내부 상태를 변경하기 어렵다.
이러한 단점들로 싱글톤 패턴은 유연성이 떨어진다고 볼 수 있다.
스프링에서의 싱글톤
스프링에서는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다. 이러한 기능 덕분에 싱글톤 패턴의 단점을 해결하고, 객체를 싱글톤으로 유지할 수 있다.
스프링 컨테이너는 빈객체를 싱글톤으로 생성한다. 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 하다.
싱글톤 패턴은 분명 단점도 많고... 유연성도 떨어지는데 왜 스프링에서는 싱글톤으로 관리할까요 ..?
스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이다. 대규모 엔터프라이즈 서버 환경은 서버 하나당 최대로 초당 수십에서 수백 번씩 요청을 받아 처리할 수 있는 높은 성능이 요구된다. 이 요청이 올때마다 매번 객체를 생성한다면, 서버가 감당하기 힘들 것이다.(부하가 옴) 때문에 서버 환경에서는 서비스 싱글톤의 사용이 권장되고 있다.
싱글톤 레지스트리
스프링은 직접 싱글톤 형태의 오브젝트를 만들고, 관리하는 기능을 제공한다. 그것이 바로 싱글톤 레지스트리이다. 싱글톤 레지스트리의 장점은 우리가 따로 static 키워드를 붙여 싱글톤 패턴을 만들어야 하는 것이 아닌, 평범한 자바 클래스를 싱글톤으로 활용하게 해준다. 때문에 테스트 환경에서 자유롭게 오브젝트를 만들 수 있고, 상속이 가능하다.
싱글톤의 예시
싱글톤을 만드는 방법에는 크게 7가지가 있다.
1. Eager Initalzation(이른 초기화 방식)
이 방법은 적은 리소스를 다룰 때 사용해야 한다. 클래스 로딩 시 객체가 생성되기 때문이다. 또한, 필드에 객체가 생성됐기 때문에 예외처리가 불가능하다.
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
2. Static Block Initialization(스태틱 블록 초기화 방식)
static 블록으로 new 연산자를 통해서 객체를 생성하게 되면, 이 역시 클래스 로딩 단계에서 인스턴스가 생성되기 때문에 적은 리소스를 사용할때에 적합하다. 대신에 1의 문제점인 Exception에 대한 Handling이 가능하게 된다.
public class Singleton {
private static Singleton instance;
private Singleton(){
}
static{
try{
instance = new Singleton();
}catch(Exception e){
throw new 익셉션발생
}
}
public static Singleton getInstance(){
return instance;
}
}
3. Lazy Initialization(늦은 초기화 방식)
1,2번에서와 다르게 호출되면 초기화를 해주는, 말그대로 늦은 초기화이다. 1,2번에서의 문제점을 어느정도 커버할 수는 있지만 더 큰 문제가 발생한다. 이는 멀티 스레드 환경에서 동시성 문제가 발생한다. 고로 멀티 스레드 환경에서는 올바르지 않는 방법이다.
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null) instance = new Singleton();
return instance;
}
}
4. Lazy InitializationThread Safe(synchronzied)
getInstance 메서드에 synchronzied를 적용하여 임계영역을 형성해 해당 영역에 하나의 스레드만 접근 가능하게 해주어 동시성 문제를 해결할 수는 있지만, 이는 성능을 약 100배 저하시키는 방법이다.
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronzied Singleton getInstance(){
if(instance == null) instance = new Singleton();
return instance;
}
}
5. Lazy InitializationThread Safe - Double Checked Locking
메소드에 synchronzied키워드를 붙히지 않고, 생성될 때에만 synchronzied가 동작하도록 한다. DCL이 제대로 되는 것 같지만, 이는 약간의 버그를 일으킬 수 있다. 하나의 스레드가 객체를 생성하고 있을 때, 만약 하나의 스레드가 접근하게 된다면? 불완전한 상태임에도 null 값은 아니기 때문에 완성되지 않은 인스턴스를 다른 스레드가 리턴할 수 있다. 즉 생성자가 호출되지 않아 불완전한 인스턴스를 다른 스레드가 가져다가 사용할 수 있다.
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
6. INITIALIZATION-ON-DEMAND HOLDER PATTERN(요청 시 초기화 홀더패턴)(by Bill pugh Singleton)
public class Singleton{
private Singleton(){
}
private static class LazyHolder{
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}
이 패턴은 윌리엄 퓨가 만들었다고 해서 Bill pugh Singleton 이라고도 불린다.static 이너 클래스를 활용해 지연 초기화를 구현하는 방법이다. Holder 클래스에 선언된 정적 필드인 INSTANCE가 사용될 때 Holder 클래스의 초기화가 일어난다. 즉, 위의 예시에서는 런타임에 Singleton.getInstance()를 호출하여 Holder.INSTANCE을 사용하기 전에 클래스로더를 통해 Holder 클래스의 초기화가 일어나게 된다.
7. Enum 클래스
public enum Singleton {
INSTANCE;
public static Singleton getInstance() {
return INSTANCE;
}
}
Joshua Block의 이펙티브 자바에서 소개된 enum을 사용하는 방법이다. 구현이 훨씬 간단하며 가장 좋은 방법이라 알려져 있다. 인스턴스가 JVM에 하나만 존재한다는 것이 100% 보장 된다.
위의 싱글톤 클래스의 경우, 클래스를 역직렬화할 때 새로운 인스턴스가 생성되어 싱글톤 속성을 위반한다. 이 문제점은readResolve메서드를 구현하여 해결할 수는 있지만, enum을 활용한 방법에서는 직렬화를 자체적으로 처리해준다.
Serializable 및 Externalizable 클래스에서, readResolve 메서드는 클래스가 스트림에서 읽은 객체가 호출자에게 반환되기 전에 교체될 수 있도록 한다. readResolve 메서드를 구현함으로써 클래스는 역직렬화되는 인스턴스를 직접 제어할 수 있다.
열거형을 직렬화할 때 필드 변수는 소실된다.
Reference
https://blog.hexabrain.net/394
'OOP > Design Pattern' 카테고리의 다른 글
08. 컴포지트(Composite) 패턴 (0) | 2023.02.14 |
---|---|
07. 어댑터(Adapter) 패턴 (0) | 2023.02.14 |
05. 프로토타입(Prototype) 패턴 (0) | 2023.02.13 |
04. 빌더(Builder) 패턴 (0) | 2023.02.13 |
03. 추상 팩토리(Abstract Method) 패턴 (0) | 2023.02.13 |