싱글톤(Singleton)
먼저 디자인 패턴은 객체를 생성하는데 총 3가지 분류로 나눌 수 있는데 생성, 구조, 행위로 나눌 수 있고 여기서 싱글톤은 객체를 생성하는 디자인 패턴 중 하나이다.
싱글톤 패턴의 주요 특징은 단 하나의 유일한 객체를 만드는 것이다. 이렇게 만들어진 하나의 객체를 다른 모듈들이 공유하며 사용하게 된다.
모듈 A, B, C가 어떤 작업을 하는데 필요한 객체를 생성한다고 생각해 보자. 그렇다면 위의 그림과 같이 각 모듈마다 새로운 객체를 생성하게 된다.
다르게 생각해보면 모듈이 100까지 있고 100개 다 동일한 기능의 객체가 필요하다면 100개의 객체를 새롭게 생성해 줘야 될 것이다. 그렇게 생성된 100개의 객체는 메모리를 차지하게 되므로 메모리 공간 낭비가 발생하게 된다.
여기서 싱글톤 패턴을 적용해보면 아래의 그림과 같이 된다.
그림과 같이 Singleton 패턴을 적용하여 단 하나의 유일한 객체를 생성하고, 해당 객체를 다른 모듈에서 사용하게 된다.
100개의 모듈에서 동일한 기능의 객체가 필요할 때 100개의 객체를 새롭게 생성하는 것이 아닌 하나의 객체를 공유해서 사용하기 때문에 메모리 공간을 그만큼 절약할 수 있다.
싱글톤 장단점
장점
- 소켓이나 데이터베이스 연결과 같은 리소스에 대한 액세스 제어가 보장된다.
DB 서버 연결을 예시로 생각해보자.
DB 서버를 연결할 때마다 객체를 새롭게 생성하고 연결하게 된다고 가정하면, 만약 서버의 주소가 변경되었을 경우 나머지 모듈에서 문제가 발생하게 된다.
싱글톤 패턴을 적용했을 경우에는 주소가 변경되면 해당 객체를 사용하는 모듈들도 변경된 주소를 적용받기 때문에 문제가 발생하지 않는다.
- 객체 생성이 제한되므로 메모리 공간 낭비가 발생하지 않는다.
앞서 설명했던 것과 같이 새로운 객체를 생성할 필요가 없기 때문에 메모리 공간 낭비가 발생하지 않는다.
단점
- TDD하기 어렵다
TDD를 할 때는 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하고 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 객체를 만들기 어렵다.
- 모듈 간의 결합도 증가한다.
한 객체를 다른 모듈들에서 공유하며 사용하기 때문에 모듈 간의 결합도가 증가할 수 있다. 해당 문제는 의존성 주입으로 해결할 수 있지만 그로 인한 클래스 수의 증가로 복잡성이 증가되는 문제가 발생한다.
싱글톤 구현 기법
여러 자료를 참고해 보면 보통 6가지 기법으로 나뉘게 된다. 1번부터 시작하여 점차 보완을 거듭하게 된다.
1. Eager Initialization
public class Singleton_Eager {
// 싱글톤 클래스 객체를 담는 인스턴스 변수
private static final Singleton_Eager INSTANCE = new Singleton_Eager();
// 생성자를 private으로 선언하여 외부에서 객체 생성을 금지
private Singleton_Eager() {}
// 외부에서 싱글톤 객체를 사용하기 위해서는 해당 메서드를 통해서 접근
public static Singleton_Eager getInstance() {
return INSTANCE;
}
}
Eager Initialization 기법은 객체를 미리 생성해 두는 기법이다.
static final이라 멀티 스레드 환경에서도 안전하지만 static이기 때문에 사용하지 않더라도 메모리에 적제 된다. 따라서 리소스가 큰 객체일 경우 메모리 공간 낭비가 발생한다.
또한, 객체를 미리 생성하기 때문에 예외 처리를 할 수 없다.
만약 싱글톤에 적용한 객체의 리소스가 크지 않은 경우에만 해당 기법을 적용하는 것이 좋다.
2. Static Block Initialization
public class SingletonStaticBlock {
// 싱글톤 클래스 객체를 담을 인스턴스 변수 생성
private static SingletonStaticBlock instance;
// 생성자를 private로 선언하여 외부에서 객체 생성을 금지
private SingletonStaticBlock() {}
// static 블록을 활용하여 예외 처리
static {
try {
instance = new SingletonStaticBlock();
} catch (Exception e) {
throw new RuntimeException("객체 생성 오류");
}
}
// 외부에서 싱글톤 객체를 사용하기 위해서는 해당 메서드를 통해서 접근
public static SingletonStaticBlock getInstance() {
return instance;
}
}
Static Block Initialization 기법은 앞선 Eager 기법에서 문제가 된 예외 처리를 static 블록으로 처리한 기법이다.
static 블록을 사용하면 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 블럭 안의 내용들이 실행되기 때문에 예외 처리가 가능해진다.
하지만 예외 처리 문제는 해결해도 여전히 static의 특성으로 인한 메모리 공간 낭비가 발생한다.
3. Lazy Initialization
public class SingletonLazy {
// 싱글톤 클래스의 객체를 담을 변수 생성
private static SingletonLazy instance;
// 생성자를 private로 선언하여 외부에서 객체 생성을 금지
private SingletonLazy() {}
// 외부에서 싱글톤 객체를 사용하기 위해서는 해당 메서드를 통해서 접근
public static SingletonLazy getSingleton() {
// 메서드를 호출했을 때 싱글톤 객체를 생성했는지 확인하여 초기화를 진행
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
Lazy Initialization 기법은 객체 생성에 대한 관리를 내부적으로 처리한다.
메서드를 호출했을 때 인스턴스 변수의 null 유무에 따라 초기화하거나 있는 걸 반환하게 되는데 이것을 통해 앞선 기법에서 사용되지 않을 경우의 메모리 공간 낭비 문제를 해결할 수 있다.
하지만 해당 기법도 문제점이 있는데스레드 세이프하지 않은 치명적인 단점을 가지고 있다는 것이다.
자바는 멀티 쓰레드 언어이기 때문에 싱글톤 객체를 생성하는 것을 동시에 처리하면 문제가 발생할 수 있다.
스레드 세이프(Thread safe)가 뭐지?
여러 스레드가 동일한 데이터에 대해 작업하고 데이터 값이 변경되면 일관성이 없는 결과를 얻을 수 있다. 이때 하나의 스레드가 이미 객체에 대해 작업 중일 경우 다른 쓰레드가 동일한 객체에 대해 작업하는 것을 방지하는 경우를 쓰레드 세이프라고 한다.
대표적으로 쓰레드 세이프를 위해서 사용하는 것이 synchronized(동기화)를 활용하는 것이다.
4. Thread safe Initialization
public class SingletonThreadSafe {
private static SingletonThreadSafe instance;
private SingletonThreadSafe() {}
public static synchronized SingletonThreadSafe getInstance() {
if (instance == null) {
instance = new SingletonThreadSafe();
}
return instance;
}
}
Thread safe Initialization 기법은 앞선 Lazy 기법에서 동기화를 설정하여 스레드 세이프 문제를 해결한 것이다.
synchronized를 통해서 메서드에 스레드들을 하나씩 접근하게 하도록 설정하여 객체를 생성한다.
하지만 여러 개의 모듈들이 매번 객체를 가져올 때 동기화 메서드를 매번 호출하여 동기화 처리 작업에 오버헤드가 발생하여 성능 하락이 발생한다.
5. Bill Pugh Solution(LazyHolder)
public class SingletonBullPugh {
private SingletonBullPugh() {}
private static class SingletonInstance {
private static final SingletonBullPugh INSTANCE = new SingletonBullPugh();
}
public static SingletonBullPugh getInstance() {
return SingletonInstance.INSTANCE;
}
}
Bill Pugh Solution 기법은 자바의 멀티스레드 환경에서 안전하고 Lazy 기법도 가능한 싱글톤 기법이다.
싱글톤을 구현하는 기법 중 가장 많이 사용하는 기법으로 클래스 안에 내부 클래스(holder)를 두어 JVM의 클래스 로더 메커니즘과 클래스가 로드되는 시점을 이용하는 방법이다.
객체를 가져오는 static 메서드에서는 static 멤버만 호출할 수 있기 때문에 내부 클래스를 static으로 설정해야 한다.
해당 기법도 단점이 존재하는데 사용자가 임의로 싱글톤을 파괴할 수 있다는 단점이 있다. (Reflection API 혹은 직렬화를 통해서 파괴가 가능)
6. Enum
public enum SingletonEnum {
INSTANCE;
public static void toSomething() {
// do something
}
}
Enum은 애초에 멤버를 만들 때 private로 만들고 한 번만 초기화하기 때문에 멀티 스레드 문제가 발생하지 않는다.
Enum 기법은 Bill Pugh 기법에서 발생하는 문제점인 사용자가 싱글톤을 파괴하는 공격에도 안전하다.
Enum은 하나의 클래스이기 때문에 상수뿐만 아니라 변수, 메서드를 선언해서 사용이 가능하다. 따라서 이를 이용하여 싱글톤 클래스처럼 사용할 수도 있다.
하지만 그에 따른 문제점으로는 클래스를 상속할 때 enum 외의 클래스 상속이 불가하고, 싱글톤 클래스를 일반적인 클래스로 변경하기 위해서는 처음부터 코드를 다시 짜야 되는 문제가 있다.
참고 자료