📌 싱글턴 패턴(Singleton Pattern) 이란?
싱글턴 패턴이란 단 하나의 유일한 객체를 만들기 위한 디자인 패턴이다.
메모리 절약을 위해, 인스턴스가 필요할 때 똑같은 인스턴스를 생성하는 것이 아닌 기존의 인스턴스를 재활용하는 기법을 말한다.
커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의 경우 인스턴스를 여러 개 만들게 되면 자원을 낭비하게 되거나 버그를 발생시킬 수 있으므로 오직 하나만 생성하고 그 인스턴스를 사용하도록 하는 데 이런 경우에 싱글턴 패턴을 활용한다.
📌 싱글턴 패턴의 구조
가장 간단한 디자인 패턴 답게 구조도 간단한다.
싱글턴으로 이용할 클래스를 외부에서 생성할 수 없게 생성자를 private 접근제어자로 막고, 자기 자신의 인스턴스를 반환하는 정적 메서드를 추가하면 된다.
📌 싱글턴 패턴의 이점
인스턴스를 단 하나만 사용하니 메모리 측면에서 이점이 있다.
싱글턴 인스턴스가 전역으로 사용되는 인스턴스이기 때문에 다른 클래스들의 인스턴스들이 쉽게 접근하여 사용할 수 있다.
전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든지 접근할 수 있게 만들 수 있지만 전역 변수를 쓸 때처럼 여러 단점을 감수할 필요도 없다. (Lazy Initializer)
📌 싱글턴 패턴의 문제점
싱글턴 패턴은 한 번에 두 가지 문제를 동시에 해결( 자신의 인스턴스가 있는지 확인, 인스턴스 반환 ) 하기 때문에 SRP를 위반한다.
멀티 스레드 환경에서 위험하다.
클래스 로더마다 서로 다른 네임스페이스를 정의하기에 클래스 로더가 2개 이상인 환경에서는 싱글턴으로 만들어진 클래스여도 여러 번 로딩이 될 수 있다.
리플렉션, 직렬화, 역직렬화도 싱글턴에서 문제가 될 수 있다.
📌 싱글턴 패턴은 안티패턴인가?
싱글턴 패턴을 안티패턴(Anti-Pattern: 사용해서는 안 되는 패턴) 이라고 부르는 사람들이 많다.
[ private 생성자를 갖고 있어 상속이 불가능하다 ]
- 상속을 통해 다형성을 적용하기 위해서는 기본 생성자가 필요하므로 싱글턴 패턴은 객체지향의 장점을 적용할 수 없다.
[ 테스트하기 힘들다 ]
- 싱글턴 패턴은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기가 어려우며, 동적으로 객체를 주입하기 힘들다.
- 테스트 코드를 작성할 수 없다는 것은 큰 단점이 된다.
[ 서버 환경에서는 싱글턴이 1개만 생성됨을 보장하지 못한다 ]
- 서버에서 클래스 로더를 어떻게 구성하느냐에 따라 싱글턴 클래스임에도 불구하고 다른 네임스페이스를 정의하는 클래스 로더들 때문에 1개 이상의 객체가 만들어질 수 있다.
- 여러 개의 JVM에 분산돼서 설치되는 경우에도 독립적으로 객체가 생성된다(네임 스페이스가 다르기 때문에)
[ 전역 상태를 만들 수 있기 때문에 바람직하지 못하다 ]
- 싱글턴의 스태틱 메서드(static method)를 이용하면 언제든지 해당 객체를 사용할 수 있고, 그로 인해 전역 상태(Global State)로 사용되기 쉽다.
- 아무나 객체에 자유롭게 접근하고 수정하며 공유되는 전역 상태는 객체지향 프로그래밍에서 권장되지 않는다.
📌 싱글턴 패턴의 구현 기법
[ Eager Initialization ]
클래스가 로드될 때 인스턴스를 즉시 생성하는 방식이다.
- 장점
- 구현이 간단하다.
- 클래스가 로드될 때 인스턴스를 생성하므로 쓰레드 안전(Thread-Safe)하다.
- 단점
- 프로그램이 실행될 때 인스턴스가 항상 생성되므로, 필요하지 않은 경우에도 리소스를 사용할 수 있다.
public class SingletonEagerInitialization {
private static final SingletonEagerInitialization INSTANCE = new SingletonEagerInitialization();
private SingletonEagerInitialization() {
// 생성자 private 선언으로 인스턴스 생성 막기
}
public static SingletonEagerInitialization getInstance() {
return INSTANCE;
}
}
[ Lazy Initialization ]
다르게 필요한 시점에 인스턴스를 생성하는 방식이다.
- 장점
- 구현이 간단하다.
- 필요한 시점에 인스턴스를 생성하기 때문에 리소스를 절약할 수 있다.
- 단점
- 멀티 스레드 환경에서는 동기화 문제가 발생할 수 있다.
public class SingletonLazyInitialization {
private static SingletonLazyInitialization INSTANCE;
private SingletonLazyInitialization() {}
public SingletonLazyInitialization getInstance() {
if(INSTANCE == null) {
return new SingletonLazyInitialization();
}
return INSTANCE;
}
}
보통의 싱글턴 패턴이라고 하면 Lazy Initialization을 설명하지만 멀티 스레드 환경에서 안전하지 않다는 치명적인 문제가 있다.
- 스레드 A, 스레드 B가 있다고 가정
- 스레드 A가 if 문을 평가하고 SingletonLazyInitialization 을 반환받으려고 함 (아직 반환 전)
- 스레드 B가 if 문을 평가하고 SingletonLazyInitialization 을 반환받으려고 함 (스레드 A에서 생성하지 못하였기 때문에 다시 생성함)
- 결론적으로 스레드 A 와 스레드 B가 SingletonLazyInitialization의 각기 다른 인스턴스를 가지기 때문에 싱글턴 패턴이 아니게 됨.
[ Thread-Safe Lazy Initialization ]
Lazy Initialization에 synchronized 키워드를 추가하여 만든 기법.
synchronized 키워드는 멀티 스레드 환경에서 두 개 이상의 스레드가 하나의 변수에 동시에 접근할 때 한 스레드가 접근하면 다른 스레드가 접근하지 못하도록 잠금(Lock)을 건다.
- 장점
- 멀티 스레드 환경에서도 안전하게 인스턴스를 생성할 수 있다.
- 단점
- 스레드 경합 조건을 해결하기 위해 잠금(Lock)을 사용하므로 성능 저하가 발생할 수 있다.
public class SingletonThreadSafe {
private static SingletonThreadSafe INSTANCE;
private SingletonThreadSafe() {}
public static synchronized SingletonThreadSafe getInstance() {
if(INSTANCE == null) {
return new SingletonThreadSafe();
}
return INSTANCE;
}
}
[ Double-Checked Locking (DCL) ]
synchronized 키워드 때문에 동기화를 실행하는 것이 성능 저하에 문제가 될 것이라고 생각해 나온 기법이다.
매번 인스턴스를 가져올 때 동기화를 하는데 사실 생성하는 경우에만 스레드 안전함이 깨지므로 최초 생성할 때만 적용하고 이미 만들어진 인스턴스를 반환할 때는 동기화를 하지 않는 기법이다.
인스턴스 필드에 volatile 키워드를 붙여주어야 I/O 불일치 문제를 해결할 수 있다.
자바 멀티 스레드 환경에서는 성능을 위해 각각의 스레드들은 변수를 메인 메모리(RAM)에서 가져오는 것이 아닌 캐시(Cache) 메모리에서 가져온다.
문제는 비동기로 변수값을 캐시에 저장하다가, 각 스레드마다 할당되어 있는 캐시 메모리의 변숫값이 일치하지 않을 수 있다는 점이다.
volatile 키워드 변수에 사용하면 해당 변수를 캐시에서 읽지 말고 메인 메모리에서 읽어오도록 지정할 수 있다.
- 장점
- 멀티 스레드 환경에서도 안전하게 인스턴스를 생성할 수 있다.
- 처음에만 동기화되고 이후에는 동기화하지 않기 때문에 성능 향상이 있을 수 있다.
- 단점
- volatile 키워드를 이용하기 위해서는 자바 1.5 이상이어야 한다.
- JVM에 따라서 스레드 안전성이 훼손될 수 있다.
public class SingletonDoubleChecked {
private static volatile SingletonDoubleChecked INSTANCE;
private SingletonDoubleChecked() {}
public static SingletonDoubleChecked getInstance() {
if(INSTANCE == null) {
return new SingletonDoubleChecked();
}
return INSTANCE;
}
}
[ Bill Pugh Singleton ]
권장되는 두가지 방법 중 하나이다.
클래스 안에 내부 클래스(holder)를 두어 JVM의 클래스 로더 메커니즘과 클래스가 로드되는 시점을 이용한 방법이다.
- 장점
- 멀티 스레드 환경에서 안전하다.
- Lazy Loading 도 가능하기 때문에 사용하지 않으면 리소스를 절약할 수 있다.
- 단점
- 클라이언트가 리플렉션, 직렬화/역직렬화를 이용해 임의로 싱글턴을 파괴할 수 있다.
public class SingletonBillPugh {
private SingletonBillPugh() {}
private static class Holder {
private static final SingletonBillPugh INSTANCE = new SingletonBillPugh();
}
public static SingletonBillPugh getInstance() {
return Holder.INSTANCE;
}
}
[ Enum Singleton ]
권장되는 두 가지 방법 중 하나이다.
자바의 Enum을 이용하여 싱글턴을 구현하는 방식이다.
- 장점
- Enum은 애초에 멤버를 만들 때 private으로 만들고 한번만 초기화하기 때문에 멀티 스레드 환경에서 안전하다.
- Enum은 상수 뿐만 아니라 변수나 메서드를 선언해 사용이 가능하기 때문에 이를 이용해 클래스처럼 응용이 가능하다.
- 리플렉션, 직렬화/역직렬화 공격에 안전하다
- 단점
- 싱글턴 클래스를 일반적인 클래스로 마이그레이션 해야 할 때는 처음부터 코드를 다시 짜야한다.
- 싱글턴 클래스가 상속이 필요할 때 enum 외의 상속은 불가능하다.
public enum SingletonEnum {
INSTANCE;
}
따라서 성능이 중요시되는 환경이라면 Bill Pugh 기법, 안정성이 중요시 되는 환경이라면 Enum으로 싱글턴 패턴을 사용하면 된다.
📌정리
싱글턴 패턴은 오직 한 개의 인스턴스 생성을 보장하여 성능상의 이점을 얻으려는 패턴이지만 트레이드오프를 생각해야 할 문제점도 많다.
개발자가 직접 만들어 사용하는 것보다는 스프링 컨테이너와 같은 프레임워크의 도움을 받으면 문제점들을 보완하면서 장점의 혜택을 누릴 수 있다.
직접 만들어야 하는 상황이라면 장단점을 고려하여 사용해야 한다.
'OOP > Design Pattern' 카테고리의 다른 글
[Design Pattern] 프로토타입 패턴(Prototype Pattern)이란? (0) | 2024.03.07 |
---|---|
[Design Pattern] 추상 팩토리 패턴(Abstract Factory Pattern)이란? (0) | 2024.03.06 |
[Design Pattern] 팩토리 메서드 패턴(Factory Method Pattern)이란? (0) | 2024.03.05 |
[Design Pattern] 빌더 패턴(Builder Pattern)이란? (0) | 2024.03.04 |
[Design Pattern] 생성 패턴이란? (0) | 2024.02.26 |