https://thisisnew-storage.tistory.com/8
1편에서 미리 동기화의 문제가 있다고 말씀드렸는데요.
구체적으로 어떤 문제인지 살펴보도록 하겠습니다.
'인스턴스를 하나만 생성하여, 이 동일한 객체를 어디서든 접근하여 이용하는 것'이 싱글톤 패턴이라고 말씀드렸었죠?
과연 '어떠한 경우에도' 객체는 하나만 생성되는지 코드를 통해 확인해보도록 하겠습니다.
먼저, 멀티스레드(Multi Thread)를 이용한 코드를 간단하게 구현해보죠.
//MultiThread.java
public class MultiThread extends Thread{
private String name;
public MultiThread(String name) {
this.name = name;
}
public void run() {
int count = 0;
for(int i=0; i<5; i++) {
count++;
Singleton singleton = Singleton.getInstance();
System.out.println(name+ "의"+ count+"번째 쓰레드의 singleton 객체 : " + singleton.toString());
try {
Thread.sleep(400);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
반복문을 이용하여 Singleton 객체를 5회 생성할 것입니다.
그리고 상속받은 Thread 클래스를 이용하여 중간에 0.4초씩 멈출 겁니다.
이런 MultiThread 객체를 Main 클래스에서 2개(AAA, BBB) 생성하여 동작시켜보죠.
//Main.java
public class Main {
public static void main(String[] args) {
MultiThread AAA = new MultiThread("AAA");
MultiThread BBB = new MultiThread("BBB");
AAA.start();
BBB.start();
}
}
결과는 아래와 같습니다.
어떤가요?
2개의 스레드가 번갈아가며 Singleton 객체에 접근하자 2개의 객체가 생성되는 것을 볼 수 있습니다.
빨간색 박스와 노란색 박스를 보면 더욱 명확하게 알 수 있죠.
어째서 이렇게 된 것일까요?
이론대로라면 분명 싱글톤 패턴은 하나의 객체만 생성해서 여럿이서 접근하여 이용하는 것인데 말이죠.
다음 사진은 2개의 MultiThread 객체가 어떻게 동작하는지 나타낸 것입니다.
순서대로 보도록 하죠.
동기화 처리가 되지 않아서 발생한 문제임을 알 수 있죠? 그럼 이제 코드를 수정해보도록 하겠습니다.
//Singleton.java
public class Singleton {
private Singleton() {}
private static Singleton single = null;
public static synchronized Singleton getInstance() {
if(single == null) {
System.out.println("싱글톤 생성...");
single = new Singleton();
}
return single;
}
}
getInstance() 메서드에 synchronized를 붙여 동기화 처리를 해줬습니다. 결과를 볼까요?
싱글톤 생성이 한 번만 일어나고, 그 뒤로는 동일한 객체를 사용하는 것을 알 수 있네요. 해결되었습니다.
하지만 메서드 자체를 동기화해서, 실행할 때마다 동기화가 되는 것이 성능적으로 과연 좋을까요?
원래의 목표는 처음 singleton 객체를 생성할 때만 동기화가 되는 것인데 말이죠.
코드를 다음과 같이 바꿔봅니다.
//Singleton.java
public class Singleton {
private Singleton() {}
private static volatile Singleton single = null;
public static Singleton getInstance() {
if(single == null) {
synchronized(Singleton.class){
if(single == null) {
System.out.println("싱글톤 생성...");
single = new Singleton();
}
}
}
return single;
}
}
조건문을 이중으로 만들었죠? 이런 패턴을 'Double-Checked Locking Pattern(이하 DCLP)'라고 하는데요.
private static volatile Singleton single = null;
그리고 기존의 single 객체에는 volatile을 붙여주었는데요.
이유를 알기 위해서는 스레드의 메모리 활용을 먼저 봐야 할 필요가 있습니다.
스레드를 이용하게 되면, 각각의 스레드는 성능을 끌어올리기 위해 'Cache Memory'를 사용하는데요.
첫 번째 스레드가 'Register'→ 'Cache Memory' → 'Main Memory'의 순서로 값을 대입해주면, 다음 스레드는 'Main Memory'에 담긴 값을 'Cache Memory'에 담아 읽어 들이는 식으로 동작합니다.
문제는 첫 번째 스레드가 'Main Memory'에 값을 대입하기 전에, 다음 스레드가 'Main Memory'에서 값을 읽어 들이려고 할 때 발생합니다.
volatile은 대입과 읽는 것을 전부 'Main Memory'에서 하도록 만드는데요. 그래서 이런 시간차를 극복하는데 도움이 될 수 있습니다.
결과를 보도록 하죠.
문제없이 동기화가 되었네요.
하지만 'Main Memory'를 바로 사용하는 것은 곧, 성능을 떨어뜨릴 수 있고, 또 이렇게 구현하는 DCLP는 자바 1.5 버전 이상에서만 호환이 됩니다.
마지막으로, 코드를 하나만 더 작성해 보도록 하죠.
다음은 'Bill Pugh Solution'이라는 윌리엄 푸가 제안한 모델입니다.
클래스 내부에 클래스를 하나 더 만드는 방법인데요.
이게 무엇을 의미하는 것인지 코드를 통해 설명해 드리겠습니다.
//Singleton.java
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.singleton;
}
private static class LazyHolder{
private static final Singleton singleton = new Singleton();
}
}
Singleton 클래스 안에 LazyHolder라는 이름의 클래스를 작성하였습니다.
LazyHolder 클래스는 static을 붙여 메모리에 미리 할당했는데요. 내부의 변수 singleton도 final을 붙여 차후에 값이 변하지 않도록 상수화 했습니다.
첫 번째 스레드가 getInstance() 메서드를 호출하면 JVM은 LazyHolder 클래스를 로드(load)하게 되는데요.
이미 메모리에는 올라가 있으니, JVM은 이것을 한 번만 로드합니다.
이때 중요한 것은 두 번째 스레드가 getInstance() 메서드를 호출하더라도, JVM은 두 번 로드하지 않고 첫 번째 로드가 끝나고 초기화가 완료될 때까지 기다리게 됩니다.
JVM이 제공하는 동기화 기법에 따른 것이죠.
이로서 이 방식은 동기화의 문제도 자연스럽게 해결할 수 있습니다.
그래서 여러 문서들에서 가장 추천하는 방식으로 꼽히고 있죠.
이 외에도 다양한 방법의 싱글톤 패턴이 있습니다만, 여기까지 소개해 드리려고 합니다.
도움이 되셨으면 좋겠네요. 감사합니다.
리플렉션(Reflection) -2 (0) | 2019.10.28 |
---|---|
리플렉션(Reflection) -1 (0) | 2019.10.25 |
[Design Pattern] 싱글톤 패턴(Singleton Pattern) -1 (2) | 2019.10.22 |
[Design Pattern] 팩토리 메서드 패턴(Factory Method Pattern) (0) | 2019.10.21 |
문자열(String)의 빈 값 혹은 Null을 다루는 여러가지 방법 (0) | 2019.10.19 |
댓글 영역