싱글톤 : 클래스를 구현한 객체가 하나만 있도록 하면서, 해당 인스턴스에 대한 전역 접근 지점을 제공하는 디자인 패턴
즉, 클래스에 대한 단 하나의 유일한 객체만을 유지하기 위해 존재한다.
어떤 클래스의 인스턴스가 필요하면 인스턴스를 새로 만드는 거싱 아닌, 기존의 인스턴스를 가져와 활용한다.
전역 변수 역할을 하는 인스턴스라고 생각하면 된다.
리소스를 많이 차지하지만 하나의 객체만 돌려쓰면 되는 클래스에 싱글톤 패턴을 적용할 수 있다.
예를 들어, 다음과 같은 경우가 있다.
- 데이터베이스 연결 모듈
- 디스크 연결 객체
- 네트워크 통신 객체
- DBCP 커넥션풀
- 스레드풀
- 로그 기록 객체
위와 같은 객체들은 새로 만들어서 사용할 일이 없는 애플리케이션에서 유일해야 하는 객체이다.
public class Main
{
public static void main(String[] args)
{
Singleton i1 = Singleton.getInstance();
Singleton i2 = Singleton.getInstance();
Singleton i3 = Singleton.getInstance();
System.out.println(i1.toString()); // Singleton@1b6d3586
System.out.println(i2.toString()); // Singleton@1b6d3586
System.out.println(i3.toString()); // Singleton@1b6d3586
System.out.println(i1 == i2); // true
}
}
싱글톤 패턴을 적용하는 방법은 간단하다.
외부에서 new 키워드를 통해 마구잡이로 인스턴스화하는 것을 막기 위해, 생성자에 private 키워드를 붙이면 된다.
또한, getInstance() 메서드를 두어서 인스턴스 필드 변수가 null 이면 객체를 생성하고, null이 아니면 이미 생성된 객체를 반환함으로써 접근 지점을 제공할 수 있다.
싱글톤 패턴 구현 기법
싱글톤 패턴을 구현하는 기법의 종류는 다음과 같다.
- Eager initialization
- Static block initialization
- Lazy initialization
- Thread safe initialization
- Double-checked Locking
- Bill Pugh solution (LazyHolder)
- Enum 이용
모두 싱글톤 패턴을 구현하지만, 뒤로 갈수록 앞의 기법의 단점을 보완한 것이다.
가장 권장되는 방법은 뒤의 Bill Pugh solution 방식과 Enum 이용 방식이다.
Eager initialization
class Singleton
{
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static final Singleton INSTANCE = new Singleton();
// private 생성자
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
한 번만 미리 만들어두는 가장 간단한 방법이다.
static final 키워드를 사용하기 때문에 멀티 스레드 환경에서도 안전하다.
그러나, 당장 객체를 사용하지 않더라도 미리 생성해두기 때문에, 사이즈가 크다면 공간 낭비가 발생한다.
또한, 예외 처리를 할 수 없다.
따라서, 사이즈가 그리 크지 않은 객체에 적합한 방식이다.
Static block initialization
class Singleton
{
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// private 생성자
private Singleton() {}
// static block을 이용해 예외 처리
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("싱글톤 객체 생성 오류");
}
}
public static Singleton getInstance() {
return instance;
}
}
static block : 클래스가 로딩되고 클래스 변수가 준비된 후, 자동으로 실행되는 코드
Static block initialization 방식은 기존의 Eager initialization 방식에 static block을 적용한 방식이다.
이를 통해, 예외 처리를 할 수 있게 되었다!
그러나, 여전히 당장 객체를 사용하지 않더라도 미리 생성해둔다.
Lazy initialization
class Singleton
{
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// private 생성자
private Singleton() {}
// 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행
public static Singleton getInstance(
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
Lazy initialization 방식은 기존 방식과 다르게, 객체가 필요할 때가 되면 생성한다.
인스턴스 변수의 null 여부에 따라 객체를 생성하거나, 이미 생성한 객체를 반환한다.
이제 객체를 미리 생성해둔다는 문제점은 사라졌다!
그러나 Thread safe 하지 않는 치명적인 문제점을 가지고 있다.
스레드 A가 조건문에 진입하는 도중, 스레드 B도 조건문에 진입했다고 해보자.
아직 스레드 A가 인스턴스 변수를 초기화하기 전이므로, 두 조건문 모두 참이되어 버려서 싱글톤 패턴을 지키지 못한다!
Thread safe initialization
class Singleton
{
// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static Singleton instance;
// private 생성자
private Singleton() {}
// synchronized 키워드 사용
public static synchronized Singleton getInstance(
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
synchronized : 여러 스레드가 하나의 데이터에 동시에 접근할 때, Race condition이 발생하지 않도록 한다.
즉, 하나의 스레드가 데이터를 점유하는 동안 Lock을 건다.
Thread safe initizalization 방식은 Lazy initialization 방식의 getInstance 메서드에 synchronized 키워드를 붙인 방식이다.
이를 통해, 메서드에 스레드가 하나씩 순차적으로 접근할 수 있도록 한다.
그러나, 객체를 생성할 때 뿐만 아니라 생성된 객체를 사용할 때도 동기화 처리가 되어버리기 때문에 성능이 하락한다.
Double-checked Locking
class Singleton
{
// volatile 키워드 적용
private static volatile Singleton instance;
// private 생성자
private Singleton() {}
public static Singleton getInstance()
{
if (instance == null)
{
// 메서드에 동기화 거는게 아닌, Singleton 클래스 자체를 동기화 걸어버림
synchronized (Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
}
Double-checked Locking 방식은 객체를 처음에 초기화할 때만 동기화처리를 한다.
이때, 인스턴스 필드 변수에 volatile 키워드를 붙여서 I/O 불일치 문제를 해결한다.
volatile : 스레드가 변수를 캐시가 아닌 메인 메모리에서 읽어오도록 지정하는 키워드
그러나, volatile 키워드를 사용하려면 JVM 1.5 이상의 버전을 사용해야 하고, JVM에 대한 심층적인 이해가 필요하다.
또한, JVM에 따라 여전히 Thread safe 하지 않는 경우가 발생할 수 있다.
Bill Pugh solution (LazyHolder)
class Singleton
{
// private 생성자
private Singleton() {}
// static 내부 클래스를 이용
private static class SingleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingleInstanceHolder.INSTANCE;
}
}
Bill Pugh solution 방식은 static 내부 클래스를 이용한 방식이다.
Thread safe 하며, 객체를 필요할 때 생성하는 것도 가능하다.
static 메서드는 static 멤버만 호출할 수 있기 때문에, 내부 클래스를 static으로 설정한다.
이는 내부 클래스의 메모리 누수 문제도 해결해준다.
가장 권장되는 두 방법 중 하나이지만, 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 문제점이 있다. (Reflection)
Enum 이용
enum SingletonEnum
{
INSTANCE;
private final Client dbClient;
SingletonEnum() {
dbClient = Database.getClient();
}
public static SingletonEnum getInstance() {
return INSTANCE;
}
public Client getClient() {
return dbClient;
}
}
public class Main
{
public static void main(String[] args)
{
SingletonEnum singleton = SingletonEnum.getInstance();
singleton.getClient();
}
}
Enum 이용 방식 역시 Thread safe 하며, 객체를 필요할 때 생성할 수 있다.
Bill Pugh solution 방식과 다르게, 클라이언트의 Reflection 공격에도 안전하다.
그러나, 싱글톤 클래스를 일반 클래스로 바꿔야 할 때, 처음부터 코드를 다시 짜야한다는 문제점이 있다.
또한, enum은 클래스를 상속할 수 없다.
따라서, 싱글톤 패턴 구현 방식은 상황에 따라 달라질 수 있다.
- Bill Pugh solution (LazyHolder) : 성능이 중요할 때 적합
- Enum 이용 : 안정성이 중요할 때 적합
장점
- 클래스가 하나의 인스턴스만 가짐을 보장할 수 있다.
- 하나의 인스턴스만 사용함으로써 메모리 낭비를 방지할 수 있다.
단점
- 모듈 간의 의존성이 높아진다.
- SOLID 원칙에 위배되는 사례가 많아진다.
- 싱글톤 인스턴스는 하나만 생성되기 때문에, 혼자서 여러가지 책임을 지니게 되어 SRP를 위반하는 경우 발생 가능
- 다른 클래스 간의 결합도가 높아지게 되어 OCP를 위반하는 경우 발생 가능
- 클라이언트가 인터페이스가 아닌, 구체 클레스에 의존하게 되는 DIP를 위반하는 경우 발생 가능
- 싱글톤 클래스는 테스트하기 어렵다.
- 단위 테스트는 테스트끼리 독립적이어야 하지만, 싱글톤 인스턴스는 자원을 공유하기 때문에 매번 인스턴스 상태를 초기화해주어야 한다.
- 많은 테스트 프레임워크가 Mock 객체를 생성할 때, 상속에 의존한다.
이처럼, 싱글톤 기법은 오직 한 개의 인스턴스 생성을 보장하여 효율성을 누릴 수 있지만, 유연성이 많이 떨어지기 때문에 안티 패턴으로도 불린다.
따라서, 직접 사용자가 만들어 사용하기 보다는 스프링 컨테이너 같은 프레임워크의 도움을 받는 것이 싱글톤 패턴의 문제점을 보완하면서 장점의 혜택을 누리는 방법이다.
예를 들어, 스프링은 IoC를 통해 클래스 제어를 컨테이너가 관리하기 때문에, 평범한 객체도 싱글톤 패턴으로 쉽게 바꿀 수 있어서 싱글톤의 단점을 제거할 수 있다!
(참고)
https://refactoring.guru/ko/design-patterns/singleton
'개발' 카테고리의 다른 글
[ 디자인 패턴: 생성 ] (5) 프로토타입 (Prototype) (0) | 2025.04.02 |
---|---|
[ 디자인 패턴: 생성 ] (4) 빌더 (Builder) (0) | 2025.04.01 |
[ 디자인 패턴: 생성 ] (2) 추상 팩토리 (Abstract Factory) (0) | 2025.03.30 |
[ 디자인 패턴: 생성 ] (1) 팩토리 메서드 (Factory method) (0) | 2025.03.29 |
디자인 패턴이란? (0) | 2025.03.24 |