싱글톤 패턴은 하나의 객체만 만들어서 이를 공유해서 쓰게 만드는 패턴이다.
객체를 계속 생성하는것은 불필요한 부담이 되기도하고, 하나만 관리하는것이 유리하기 때문이다.
고전적인 싱글톤에서는, private 생성자를 만들고, public static get_instance 메서드를 통해서만 객체를 받을 수 있도록 설계한다.
package singleton;
public class classicSingleton {
private static classicSingleton uniqueInstance;
private classicSingleton() {
}
public static classicSingleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new classicSingleton();
}
return uniqueInstance;
}
}
package singleton;
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
empty = true;
boiled = false;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if (!isEmpty() && isBoiled()) {
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
이러한 초콜릿 공장 코드가 있다고 하자. 각 상황별 조건에 맞게 함수들이 작동하도록 설정했다.
하지만 public 생성자로 인해 두개 이상의 객체가 만들어질 수 있고, 이는 좋지 못하다.
이를 싱글톤으로 변경하면 다음과 같다.
package singleton;
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if (!isEmpty() && isBoiled()) {
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
하나의 객체를 쓰레드 두개가 동시에 접근한다고 하자. 싱글톤은 하나의 객체이기에, 이때 공유하는 empty, boiled값은 똑같다.
하지만 두개의 쓰레드가 이를 동시에 실행한다면 어떨까?
매번 그런건 아니겠지만 일정 확률로
uniqueInstance==null -> uniqueInstance==null -> new ChocolateBoiler1() --> new ChocolateBoiler2()
같은 경우가 생길 수 있다. 분명 싱글톤인데 두개가 생기는것이다.
public static synchronized ChocolateBoiler getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoiler();
}
return uniqueInstance;
}
이렇게 싱크로나이즈를 써서 동기화를 시키면 가능하다. 다만, 이러한 방식은 속도가 엄청 느리다.
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
return uniqueInstance;
}
이런식으로 만들어두면 된다.
package singleton;
public class ChocolateBoilerDCL {
private boolean empty;
private boolean boiled;
private volatile static ChocolateBoilerDCL uniqueInstance;
private ChocolateBoilerDCL() {
empty = true;
boiled = false;
}
public static ChocolateBoilerDCL getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateBoilerDCL.class) {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoilerDCL();
}
}
}
return uniqueInstance;
}
package singleton;
public class ChocolateBoilerDCL {
private boolean empty;
private boolean boiled;
private volatile static ChocolateBoilerDCL uniqueInstance;
private ChocolateBoilerDCL() {
empty = true;
boiled = false;
}
public static ChocolateBoilerDCL getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateBoilerDCL.class) {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateBoilerDCL();
}
}
}
return uniqueInstance;
}
싱글톤은 많은 개발자들이 익숙한 방식이기도하고, 객체가 다중생성되어 리소스를 많이 사용하는일을 사전에 방지할 수 있다.
다만 private 생성자를 가져서 상속이 어렵고 (protected를 써야한다) 엄밀히 말하자면 SRP원칙을 위배하게 된다.
다만 장점이 뚜렷해서 사용되고 있으며, 특히 현대 웹 환경은 동시다발적 객체호출이 많기에 이러한 디자인이 권장된다.
이에 웹 개발툴인 스프링에서는 이를 CGLIB 기술을 통해, 지원하고 관리해준다.