싱글턴 패턴 정의
싱글턴 패턴을 사용하면 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들수 있습니다. 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야합니다. 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야합니다.
또한 어디서든 그 인스턴스에 접근할 수 있도록 만들어져야 합니다. 다른 객체에서 이 인스턴스가 필요하면 언제든지 클래스한테 요청할 수있게 만들고, 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 합니다.
class MyClass {
private MyClass() {
public static MyClass getInstance() {
return new MyClass();
}
}
MyClass객체 하나를 다른 클래스애서 new연산자를 사용해서 만들수가 없습니다. 이유는 MyClass의 생성자가 private으로 선언되었기 때문인데요. private키워드는 다른클래스로부터 호출이 불가능하게 되어있습니다. 이러한 패턴은 다른클래스에서 함부로 MyClass객체를 찍어내지 않게끔 하기 위함인데요. 그러면 애초에 new연산자를 사용해서 만들수가 없는데 이게 무슨의미가 있을까요?(사용이 가능 할까요?)
네. 사용가능합니다.
다른 클래스에서 MyClass객체를 생성하기 위해서는 getInstance메소드를 호출해주면 사용 가능합니다.
고전적인 싱글턴 패턴에대해서 알아보겠습니다.
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// other useful methods here
public String getDescription() {
return "I'm a classic Singleton!";
}
}
Singleton 클래스로부터 호출한 getInstance메소드로부터 만들수 있는 객체는 오직 하나뿐입니다. private으로 만들어둔 맴버 필드 uniqueInstance를 선언한것이 보이죠? getInstance메소드를 호출하면 new연산자를 사용해서 uniqueInstance에 객체를 저장해둡니다. 만약 두번째부터 또 호출이 일어난다면 uniqueInstace멤버변수엔 이미 객체가 있기때문에 그대로 uniqueInstance를 리턴해줍니다. 따라서 프로그램에서 Singleton클래스로만든 객체는 uniqueInstance가 유일하게 되는겁니다.
또한가지 눈여겨 봐야할 부분이 static부분입니다. uniqueInstance멤버 필드와 getInstance멤버메소드는 static으로 선언된것을 볼 수 있습니다. 이는 객체로 접근하지 않아도 된다는 뜻입니다. static에 대해서 좀 알아보겠습니다.
Singleton.getInstance() 이런식으로 클래스이름과 스택트맴버이름으로 외부의 클래스 어디에서든지 접근이 가능합니다.
게으른 생성(Lazy Creation)
객체가 필요한 시점에 그때 비로소 객체를 만드는 방법입니다. 초기에 필요한 시점 전에 선언하는것 보다는 메모리시점에서는 효율적이라고 볼 수 있습니다.
싱글턴 패턴 예제(보일러)
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
System.out.println("Creating unique instance of Chocolate Boiler");
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
// fill the boiler with a milk/chocolate mixture
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
// drain the boiled milk and chocolate
empty = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
멀티스레드에서 실행하기 (문제점)
public class ChocolateController {
public static void main(String args[]) {
Thread thread1 = new Thread() {
public void run() {
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
}
};
Thread thread2 = new Thread() {
public void run() {
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
}
};
thread1.start();
thread2.start();
}
}
이론적으론.....이래야하는데
실제로는 이렇습니다. 항상 이런건 아닙니다만
소스코드 실행줄에 실행번호를 주겠습니다.
public static ChocolateBoiler getInstance() {
1. if (uniqueInstance == null) {
2. uniqueInstance = new ChocolateBoiler();
3. }
4. return uniqueInstance;
}
문제가 발생하지않는 상황도 있습니다. 스레드가 이런식으로 동작하면 문제될게 없습니다.
|
thread 1 |
thread 2 |
Step1 |
1 |
|
Step2 |
2 |
|
Step3 |
3 |
|
Step4 |
4 |
|
Step5 |
|
1 |
Step6 |
|
4 |
Step7 |
|
|
Step8 |
|
|
Step9 |
|
|
Step10 |
|
|
근데 문제가 발생하는 상황은 이런상황이죠....
|
thread 1 |
thread 2 |
Step1 |
1 |
|
Step2 |
|
1 |
Step3 |
2 |
|
Step4 |
3 |
|
Step5 |
|
2 |
Step6 |
|
3 |
Step7 |
|
4 |
Step8 |
4 |
|
Step9 |
|
|
Step10 |
|
|
스레드1 에서 1의 조건문을 통과하고 만약 스레드2가 실행된다면 1의 조건문을 통과하게 되므로 둘다 2번 실행문을 실행시키게 됩니다. 그러면 싱글턴패턴에 위배되는 객체가 2개가 만들어지는 상황이 발생합니다.
해결방안 1( Synchronized)
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
System.out.println("Creating unique instance of Chocolate Boiler");
}
public static synchronized ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
}
public static synchronized ChocolateBoiler getInstance() 이부분에 synchronized 키워드 하나만을 붙이면 어떤 자바 스레드가 이 메소드를 실행하고 있을떼 다른 스레드는 이 메소드를 건들지 않게 됩니다. 이렇게 하면 간단하게 동기화 문제를 해결 할 수 있습니다. 하지만 큰 단점이 있는데요. 동기화가 필요한 시점은 메소드가 시작되는 때일 뿐입니다.
무슨뜻이냐면 여러개의 각각의 독립적인 스레드가 있으면 각 스레드마다 getInstance()메소드를 호출 할 수 있겠죠 동기화를 계속하게 됩니다. 사실 처음에 한번만 동기화를 하면 다른스레드들이 getInstace()를 호출해도 return uniqueInstace만 호출해서 기존에 미리 만들어둔 하나의 객체를 불러오겠죠. 즉 처음에 동기화를 하면 두번째는 사실 필요가 없습니다. 처움 동기화 외의 동기화는 불필요한 오버헤드(성능 100배 이상 저하)를 유발함니다......
해결방한 2 (클래스 로딩 시 생성)
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();
private ChocolateBoiler() {
empty = true;
boiled = false;
System.out.println("Creating unique instance of Chocolate Boiler");
}
public static ChocolateBoiler getInstance() {
System.out.println("Returning instance of Chocolate Boiler");
return uniqueInstance;
}
}
클래스가 로딩시에 private static ChocolateBoiler uniqueInstance = new ChocolateBoiler(); 이 한 구문으로 uniqueInstace객체가 생성되게 됩니다. 클래스로딩은 한번만 일어나므로 싱글턴 패턴에 부합합니다. 그런데 단점으로는 이 객체가 필요한 시점 전부터 메모리 리소스를 차지한다는 단점이 있겠군요 클래스로딩은 프로그램 시작할때부터 일어나게 되니깐요.
해결방안 3 (Double-Checking Locking : DCL)
public class ChocolateBoiler {
private volatile static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
System.out.println("Creating unique instance of Chocolate Boiler");
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateBoiler.class) {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
}
}
return uniqueInstance;
}
}
스레드 관점에서 생각해 보겠습니다. 가령 스레드1 이랑 스레드2 가 첫번째 if문 if(uniqueInstance == null) 을 실행했다고 합시다. 먼저 스레드1이 synchronized된 블럭에 들어가서 코드에 접근합니다. synchroninzed가 되었기 때문에 스레드1이 일을 마칠때까지 스레드2는 기다리게 됩니다. 스레드1이 동기화된 블럭을 실행시키고 uniqueInstance에 new연산자를 사용해 객체를 저장합니다. 그러면 더이상 uniqueinstance는 null 이 아니게 되죠. 하지만 스레드2입장에서는 동기화블럭을 접근하려고 할때 이미 첫번째 if문은 통과해서 스레드1이 동기화블럭을 빠져나오길 기다리고 있는 상태였습니다. 따라서 두번째 if문에서 체크를 하게 되는겁니다. 스레드1이 uniqueInstance에 겍체를 저장했으니깐 스레드2는 아무것도 하지않고 블럭을 빠져나오게 됩니다. 이것이 더블체킹로킹 입니다. 장점으로는 Lazey creation이 가능하고 스레드가 안전해집니다.
private volatile static ChocolateBoiler uniqueInstance; 에 있는 volatile키워드가 있습니다. 스레드는 성능향상을 위해서 메모리에 직접 접근하는것이 아니라 캐시에 먼저 접근합니다. 위에서와 같이 스레드1이 캐시1을 통해 메모리에 접근해서 uniqueInstance에 객체를 저장했다고 해봅시다. 그런다음 스레드2가 가지고있는 캐시2의 값과 메모리에 있는값이 서로 다른 inconsistency가 발생하면 내용이 꼬이게 됩니다. 이럴때 volatile키워드를 사용하면 문제 방지가 가능합니다. volatile 키워드는 스레드에게 캐시를 통해서 정보에 접근하지 말고 직접 메모리에 접근하라고 시키는 키워드 입니다. 이는 간단한 설명이고 자세한 내용은 따로 올리겠습니다.
다음장에는 위의 해결방안 3가지를 통해서 얻는 성능을 평가해보는 시간을 가지겠습니다.