스프링 빈은 싱글턴으로 관리된다.
스프링은 계층형 구조 (MVC)로 이루어진 형태이고, 각각의 레이어들은 다른 역할을 감당하면서 협력한다.
트래픽이 높은 상황, 초당 수백 수천건의 요청이 서버로 오는 경우를 상상해보자.
이 때, 클라이언트에서 요청이 올 때마다, 레이어별로 매번 객체를 새로 만들어서 사용한다면 서버는 매우 큰 부하가 걸릴 것이다.
이처럼 상황에 따라서 객체의 "인스턴스"를 오직 하나로 보장하는 것이 싱글턴 패턴의 기본 아이디어이다.
객체의 인스턴를 오직 하나로 보장하는 방법에는 무엇이 있을까? 함께 알아보자!
public class OldSingleton {
private static OldSingleton uniqueInstance;
private OldSingleton() {} // 생성자 매서드
public static OldSingleton getInstance(){
if (uniqueInstance == null){
uniqueInstance = new OldSingleton();
}
return uniqueInstance;
}
}
if 분기문을 통해서 필드에 선언한 static한 인스턴스, uniqueInstance가 초기화 되어있는지 확인한다 (초기화 되지 않았다면, null일것이다.)
초기화 되어 있지 않다면 새 인스턴스를 생성한다. 이렇게 인스턴스가 생성되면, 다음 분기부터 해당 인스턴스는 null이 아니므로 분기문을 타지 않는다.
인스턴스를 반환한다. 이 인스턴스는 해당 분기문에 의해 "오직 하나"의 인스턴스임을 보장받는다.
굉장히 쉽습니다. 하지만 이 코드가 괜찮은 코드일까요? 실제 사용 가능할까요?
멀티 스레드 환경에서 문제가 발생할 소지가 매우 높습니다.
예를 들어, 두 개의 귀여운 스레드가 있다고 합시다. (A,B)
두 스레드가 해당 로직을 거치는 과정을 한번 실험해보죠.
static class SingletonThreadTest {
public static void main(String[] args) {
final int numThreads = 5; // 생성할 스레드 수
Runnable runnable = () -> {
OldSingleton singleton = OldSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + singleton);
};
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(runnable);
threads[i].start();
}
}
}
귀여운 쓰레드들이 어떤 인스턴스를 생성했나 살펴봅시다.
0번 스레드 혼자 다른 인스턴스를 생성했네요!
이러면 인스턴스가 하나인 것이 보장받지 못합니다.
고전파의 문제를 해결해봅시다!
방법이 세 가지나 됩니다! 세 방법은 다음과 같습니다.
코드 이후엔 설명을 첨부했습니다.
public class MiddleAgeSingleton {
private static MiddleAgeSingleton uniqueInstance; // 방법1
private volatile static MiddleAgeSingleton volatileUniqueInstance; // 방법2
public static synchronized MiddleAgeSingleton getInstance1(){ // 방법 1
if (uniqueInstance == null){
uniqueInstance = new MiddleAgeSingleton();
}
return uniqueInstance;
}
public static MiddleAgeSingleton getInstance2(){ // 방법2
if (volatileUniqueInstance == null){
synchronized (MiddleAgeSingleton.class) {
if (volatileUniqueInstance == null){
volatileUniqueInstance = new MiddleAgeSingleton();
}
}
}
return volatileUniqueInstance;
}
private static final MiddleAgeSingleton readyMadeUniqueInstance
= new MiddleAgeSingleton(); // 방법 3
}
private static MiddleAgeSingleton uniqueInstance; // 방법1
public static synchronized MiddleAgeSingleton getInstance1(){ // 방법 1
if (uniqueInstance == null){
uniqueInstance = new MiddleAgeSingleton();
}
return uniqueInstance;
}
synchronized 키워드를 써서 스레드를 동기화 시킵니다.
즉, 줄세웁니다. "나 끝나기 전까지는 하지말어!!"
동기화의 문제는 성능입니다. 비동기식 처리와 대략 100배정도의 성능 차이가 있습니다.
따라서 성능상 이슈가 없을 때, getInstance() 매서드가 그리 자주 호출되지 않을 땐 문제가 되지 않습니다만, getInstance()가 병목으로 작용한다면(자주 호출된다면) 문제가 됩니다.
public class MiddleAgeSingleton {
private static final MiddleAgeSingleton readyMadeUniqueInstance
= new MiddleAgeSingleton(); // 방법 2
public static MiddleAgeSingleton getInstance2(){
return readyMadeUniqueInstance;
}
}
미리 인스턴스를 생성한다는 것은, 클래스로더에 의해서 클래스가 초기화 되었을 때 인스턴스가 생성된다는 뜻입니다. 불필요한 공간 낭비가 있을 수 있다는 뜻이죠
클래스 로더(JVM 안에 있음)의 클래스 초기화 시점이 어떻게 되는데요?
퀴즈! 방법2의 코드는 언제 초기화될까요?
private volatile static MiddleAgeSingleton volatileUniqueInstance; // 방법3
public static MiddleAgeSingleton getInstance3(){ // 방법3
if (volatileUniqueInstance == null){
synchronized (MiddleAgeSingleton.class) {
if (volatileUniqueInstance == null){
volatileUniqueInstance = new MiddleAgeSingleton();
}
}
}
return volatileUniqueInstance;
}
volatile 키워드를 써서 DCL(Double-Checking-Locking)을 사용하는 것입니다.
DCL을 사용하면 인스턴스가 생성되어 있는지 확인 한 다음, 생성되지 않았을 때만 동기화합니다.
if문에서 인스턴스를 확인하고, 없다면 동기화된 블록으로 들어갑니다.
if (volatileUniqueInstance == null){
//if문에서 인스턴스를 확인하고, 없다면 동기화된 블록으로 들어갑니다.
synchronized (MiddleAgeSingleton.class) {
// synchronized 키워드를 사용해서 동기화합니다
if (volatileUniqueInstance == null){
volatileUniqueInstance = new MiddleAgeSingleton();
}
}
}
synchronized 키워드를 사용해서 동기화한 로직을 실행합니다.
로직 안에는 인스턴스의 존재를 더블체킹하는 로직이 존재하며 ( 동기화된 블록 접근 전에 인스턴스가 생겼는지 확인하기 위함 ), 체크되면 인스턴스를 생성합니다.
성능상 이슈가 해결 가능합니다! 적절한 방법이군요.
public class EnumSingleton {
public enum SingletonEnum {
UNIQUE_INSTANCE;
}
static class SingletonClient {
public static void main(String[] args) {
SingletonEnum enumSingleton = SingletonEnum.UNIQUE_INSTANCE;
}
}
}
아주아주 간단한 방법입니다.
이 간단한 방법이
전부 해결 가능합니다.
public class HolderSingleton {
private HolderSingleton() {
// private 생성자
}
private static class SingletonHolder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Holder 클래스가 실제 인스턴스 생성하는 클래스를 품고 있는 형태입니다.
HolderSingleton(이름이 구져서 죄송합니다) 클래스의 초기화 시점은 해당 클래스의 정적 변수, 매서드에 접근하는 시점이므로, lazy init이 보장됩니다.
초기화 시 홀더클래스도 함께 초기화되며, 하나의 인스턴스를 보장합니다.
크...정리가 너무 깔끔하네요!!