이번 글에서는 싱글톤 패턴이 무엇인지와 장단점을 알아보고 자바로 싱글톤 패턴을 구현하는 방법들을 살펴봅니다.
싱글톤이란?
싱글톤 패턴(Singleton Pattern)
은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 패턴입니다.
여러 개의 인스턴스를 생성하지 않고 처음 생성된 하나의 인스턴스를 공유하여 사용하기 때문에 메모리를 절약하고 성능을 향상할 수 있습니다.
데이터베이스의 커넥션 풀, 스레드 풀, 로깅, 캐시등 I/O 바운드 작업과 프로그램 전역에서 공유되는 자원을 관리할 때 주로 사용됩니다.
장단점
장점
1. 인스턴스 생성 비용과 메모리 절약
싱글톤 패턴은 인스턴스를 중복으로 생성하지 않고 한번 생성된 인스턴스를 여러 모듈에서 공유하여 사용하기 때문에 인스턴스 생성 비용과 메모리를 절약할 수 있습니다.
예를 들어 I/O 바운드 작업과 같이 비용이 큰 작업을 인스턴스 생성 시 수행하는 클래스의 경우에는 인스턴스를 한 번만 생성하여 인스턴스 생성 비용을 줄일 수 있고, 인스턴스의 사이즈가 큰 경우에는 인스턴스의 중복 생성을 방지하여 메모리를 절약할 수 있습니다.
2. 전역적 공유 자원 관리에 용이
하나의 인스턴스를 전역에서 공유하기 때문에 전역적 공유자원을 관리하기에 용이합니다.
예를 들어 데이터베이스의 커넥션 풀을 관리하는 클래스는 데이터베이스에 접근하는 모든 모듈에서 사용됩니다. 데이터베이스에 접근하는 모듈마다 커넥션 풀을 관리하는 인스턴스를 생성하게 되면 프로그램 전체에 생성되는 커넥션의 수를 관리하기 힘듭니다. 반면 여러 모듈에서 같은 커넥션 풀 인스턴스를 공유하면 커넥션들을 효율적으로 관리할 수 있습니다. 이 클래스에 싱글톤 패턴을 사용하면 커넥션 풀을 전역에서 공유하여 사용할 수 있습니다.
단점
1. 전역 상태 공유로 인한 버그 발생 가능성
싱글톤 패턴은 하나의 인스턴스를 공유하기 때문에 싱글톤 인스턴스를 통해 서로 독립적인 모듈 사이에 영향을 미치는 버그가 발생할 수 있습니다.
2. 멀티 스레드 환경에서 동기화 고려
싱글톤 패턴을 잘못 설계하면 멀티 스레드 환경에서 싱글톤 인스턴스가 유일하지 않게 되어 버그가 발생할 수 있고 이를 동기화하는 과정에서 성능 저하가 발생할 수 있습니다.
3. 단위 테스트의 어려움
인스턴스에 대한 의존성은 단위 테스트를 어렵게 만듭니다. 싱글톤 인스턴스를 사용하는 클래스 혹은 메서드를 테스트할 때 싱글톤 인스턴스에 대한 의존성을 해결해야 합니다.
Java 구현
1. Eager Initialization
정적 필드와 정적 블록은 JVM이 클래스를 로드할 때 실행됩니다. 이를 이용해 클래스가 로드될 때 싱글톤 인스턴스를 생성해 스레드 세이프하게 유일한 인스턴스를 생성하는 방법입니다. 하지만 클래스가 로드될 때 싱글톤 인스턴스를 생성하기 때문에 싱글톤 인스턴스를 사용하지 않더라도 인스턴스가 생성되어 메모리 낭비가 발생할 수 있습니다. 또한 인스턴스 생성 시 발생하는 예외처리가 불가능합니다.
public class EagerSingleton {
// 클래스 로드시 싱글톤 인스턴스 생성
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
2. Static Block Initialization
정적 블록을 이용해 클래스가 로드될 때 싱글톤 인스턴스를 생성하는 방법입니다. 위 Eager Initialization
과 비슷하지만 정적 블록을 이용하기 때문에 예외처리가 가능합니다.
public class StaticBlockSingleton {
private static final StaticBlockSingleton INSTANCE;
static {
try {
INSTANCE = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
private StaticBlockSingleton() {}
public static StaticBlockSingleton getInstance() {
return INSTANCE;
}
}
3. Lazy Initialization
lazy Initialization
은 가장 쉽게 떠올릴 수 있는 방법으로 인스턴스 호출 시 생성된 인스턴스가 없다면 생성하고 있다면 기존의 인스턴스를 반환하는 방법입니다. 인스턴스가 처음 호출될 때 생성하기 때문에 앞서 살펴본 클래스 로딩 시 생성하는 방법보다 메모리적으로 나은 방법일 수 있습니다. 하지만 멀티 스레드 환경에서 동기화 문제가 발생할 수 있습니다.
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 1
instance = new LazySingleton(); // 2
}
return instance;
}
}
스레드1이 if문(1)을 통과하고 인스턴스를 생성(2)하기 전에 스레드2도 if문(1)을 통과하게 된다면 인스턴스가 유일하지 않을 수 있습니다.
4. Thread Safe Lazy Initialization(Double-checked Locking)
이 방법은 방금 살펴본 lazy Initialization
방법의 멀티 스레드 환병에서 동기화 문제를 해결한 방법입니다. synchronized
키워드를 사용하여 동기화해 스레드 세이프하게 인스턴스를 생성합니다. 하지만 getInstance
메서드 전체를 동기화하면 유일한 인스턴스가 생성되어 더 이상 동기화가 필요 없는 경우에도 동기화되어 getInstance
가 동시에 호출될 때마다 락이 걸려 성능저하가 발생합니다. 이런 성능저하를 줄이기 위해 Double-checked Locking
기법을 사용합니다. Double-checked Locking
을 사용하면 인스턴스가 생성되어 있다면 동기화하지 않습니다.
이때 instance
필드 변수는 volatile
로 선언해야 합니다. 자바는 성능 향상을 위해 CPU 캐시를 사용합니다. 캐시를 사용하면 스레드가 인스턴스를 생성하고 메모리에 쓰기 전에 다른 스레드가 메모리를 읽어 인스턴스가 중복되어 생성될 수 있습니다. 이를 방지하기 위해 volatile
키워드를 사용하여 캐시를 사용하지 않고 메인 메모리를 사용합니다.
public class ThreadSafeLazySingleton {
// volatile로 선언
private volatile ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton() {}
// 인스턴스를 가져올 때 마다 락이 걸리므로 성능 저하
public static synchronized ThreadSafeLazySingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazySingleton();
}
return instance;
}
// double-checked locking
public static ThreadSafeLazySingleton getInstanceUsingDoubleCheckedLocking() {
if (instance == null) {
// 인스턴스가 생성되지 않았을 때만 동기화
synchronized (ThreadSafeLazySingleton.class) {
if (instance == null) {
instance = new ThreadSafeLazySingleton();
}
}
}
return instance;
}
}
5. Bill Pugh Singleton
Bill Pugh Singleton
은 Eager Initialization
과 비슷하지만 static inner class
를 이용해 싱글톤 인스턴스를 생성하는 방법입니다. static inner class
는 클래스가 로드될 때 로드되지 않고 처음 사용 될 때 로드됩니다. 이를 이용해 클래스가 로드될 때 인스턴스를 생성하지 않고 처음 사용 될 때 인스턴스를 생성합니다.
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
Reflection을 이용한 싱글톤 파괴
앞서 살펴본 싱글톤 구현 방법들은 싱글톤이 파괴될 수 있습니다. 자바에는 동적으로 클래스의 정보를 알 수 있게 해주는 Reflection API
가 있습니다. Reflection API
를 이용하면 생성자가 private
인 클래스의 인스턴스를 생성할 수 있습니다. 의도적으로 싱글톤 인스턴스를 유일하지 않게 만들어 버그를 발생시키는 경우가 아니라면 사용하진 않겠지만 그래도 알아봅시다.
import java.lang.reflect.Constructor;
public class ReflectionSingletonBreak {
public static void main(String[] args) {
EagerSingleton instanceOne = EagerSingleton.getInstance(); // 유일한 인스턴스
EagerSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// 새로운 instance 생성
constructor.setAccessible(true);
instanceTwo = (EagerSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
6. Enum Singleton
enum
을 이용한 싱글톤 구현 방법은 Effective Java
를 쓴 Joshua Bloch가 제안하고 추천하는 방법으로 enum
은 스레드 세이프가 보장되며 Reflection
을 이용해 인스턴스를 생성할 수 없는 그 자체로 싱글톤입니다. 다만 상속을 받을 수 없는 등 클래스의 이점을 포기해야 합니다.
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// do something
}
}
마치며
이번 글에서는 싱글톤 패턴과 싱글톤 패턴의 장단점을 알아보고 여러 자바를 이용해 방법으로 구현해 보고 각각의 구현 방법의 특징에 대해 알아봤습니다. 애플리케이션이 커짐에 따라 전역 상태를 갖는 것은 예상치 못한 사이드 이펙트와 디버깅을 어렵게 할 수 있지만 분리된 환경 안에서 공유 자원에 대한 I/O 바운드 작업을 수행하는 클래스는 싱글톤 패턴으로 만들면 관리하기 쉬울 것 같습니다. 싱글톤 패턴의 구현은 Bill Pugh Singleton
혹은 Enum Singleton
으로 구현하는 것이 간단하면서 성능적으로도 좋은 선택인 것 같습니다.
참고
'Computer Science > etc.' 카테고리의 다른 글
[Linux] 커널 패치로 TCP 성능 40% 향상시키기(by Google) (0) | 2024.03.27 |
---|---|
모듈러(Modular)와 C, Java, Python, Javascript의 나머지 연산자 (0) | 2023.09.08 |
의존성 주입(Dependency Injection) (0) | 2023.08.08 |