Singleton Pattern?
- instance를 오직 한개만 제공하는 class
- system runtime, 환경 setting에 대한 정보 등, instance가 여러개 일 때 문제가 생길 수 있는 경우가 있다. ( IDE 툴의 UI, 환경 설정같은 건 하나만 존재해야함 )
- 그래서 instance를 오직 한 개만 만들어 제공하는 class가 필요하다.
Static을 버무려서 간단하게 Singleton 만들기
public class Tools {
private static Tools instnace;
private Tools(){}
public satatic Tools getInstance() {
if (instance == null) {
instance = new Tools();
}
return instance;
}
}
public class App {
public static void main(String[] args) {
Tools tools1 = Tools.getInstance();
Tools tools2 = Tools.getInstance();
System.out.println(tools1 == tools2);
}
}
- 위 코드는 Static method를 활용해 singleton을 구현했다.
- 이런 static method는 이펙티브 자바 아이템1의 주제인 static factory method와도 연관이 있다.
- 이것도 문제점이 하나 있다. 바로 thread safe하지 않단 것이다.
- 만약 thread1이 instance가 null인지 체크한 뒤, 새로운 instance를 생성할 때, thread2도 null 체크를 한다면 instance는 두 개가 생성될 것이다!
Thread Safe한 Singleton 만들기
Synchronized
public satatic synchronized Tools getInstance() {
if (instance == null) {
instance = new Tools();
}
return instance;
}
- synchronized keyword(예약어)를 사용하면 하나의 thread만 허용하게 된다.
- 이 방법은 한 thread가 점유하고 있을 때, 다른 thread의 접근을 막는 과정에서 자원이 소모된다. ( 한마디로 lock(block)한다는 뜻이다. )
Eager Initialization
public class Tools {
private static final Tools INSTANCE = new Tools();
private Tools(){}
public satatic Tools getInstance() {
return instance;
}
}
- 미리 instance를 생성하는 방법을 사용합니다.
- thread safe도 됩니다.
- static field인 만큼 instance를 생성하지 않아도 사용할 수 있다.(memory 공간차지)
- 미리 초기화 하기 때문에 instnace를 생성하는 과정에 자원 소모가 크다면 Application을 loading하는 과정이 느려질 것이다.
Double Checked Locking
private static volatile Tools instnace;
public satatic Tools getInstance() {
if (instance == null) {
synchronized (Setting.class) {
if (instance == null) {
instance = new Setting();
}
}
}
return instance;
}
- 만약 instance가 null이라면 synchronized 키워드를 사용하여 thread safe한 상황에서 다시 instance가 null인지 다시 확인(double check) 한 후 instance를 생성하는 방법이다.
- 처음부터 thread block을 유발하지 않아 자원소모를 방지할 수 있다.
- instance를 생성하여 저장하는 field에 volatile이란 keyword를 붙여줘야 한다.
- volatile은 multi thread 환경에서 변수의 값을 일치 시키기 위해 사용합니다. ( CPU Cache )
Static Inner
priavet static class ToolsHolder {
private static final Tools INSTANCE = new Tools();
}
public satatic Tools getInstance() {
return ToolsHolder.INSTANCE;
}
- class의 내부에 static class(inner class)를 만든 후 inner class에 instance를 생성하는 방법이다.
- 이 방법은 thread safe도 된다.
- getInstance가 호출될 때, inner class가 loading이 되고 instance를 생성하기 때문에 lazy loading도 가능하다.
Singleton 마구 부수기
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class App {
public static void main(String[] args) thorws NoSuchMethodException, InvocationTargetException, InstantiationException {
Tools tools1 = Tools.getInstance();
Constructor<Tools> constructor = Tools.class.getDeclaredConstructor();
constructor.setAccessible(true);
Tools tools2 = constructor.newInstance();
System.out.println(tools1 == tools2);
}
}
- java의 reflection을 사용하면 Tools의 새로운 instance를 만들 수 있다.
import java.io.Serializable;
public class Tools implements Serializable {
private Tools() { }
private static class ToolsHolder {
private static final Tools INSTANCE = new Tools();
}
public static Tools getInstance() {
return ToolsHolder.INSTANCE;
}
}
public class App throws IOException {
public static void main(String[] args) {
Tools tools1 = Tools.getInstance();
Tools tools2 = null;
try (ObjectOutput output = new ObjectOutputStream(new FileOutputStream("tools.obj"))) {
output.writeObject(tools1);
}
try (ObjectInput input = new ObjectInputStream(new FileInputStream("tools.obj"))) {
tools2 = (Tools) input.readObject();
}
System.out.println(tools1 == tools2);
}
}
- java는 file을 저장할 땐 직렬화(serialization), file을 불러 올 땐 역직렬화(deserialization)를 한다.
- 직렬화한 instance를 역직렬화하여 불러올 때는 생성자를 사용하여 instance를 다시 만들기 때문에 singleton이 깨진다.
public class Tools implements Serializable {
...
protected Object readResolve() {
return getInstance();
}
}
- 다만, 역직렬화의 경우 readResolve란 method를 사용하기 때문에 이미 생성한 instance를 반환하도록 만들면 역직렬화를 막을 수 있다.
위 문제점들을 보완한 Singleton 방법
public enum Tools {
INSTANCE;
}
public class App {
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
Tools tools1 = Tools.INSTANCE;
Tools tools2 = null;
Constructor<?>[] declaredConstructors = Tools.class.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
tools2 = (Tools) declaredConstructor.newInstance("INSTANCE");
}
System.out.println(tools1 == tools2);
}
}
- enum을 사용하면 reflection도 막고 역직렬화도 막을 수 있습니다.
- 실제로 위 코드를 실행시켜보면 enum object는 reflection을 할 수 없다는 오류가 발생합니다.
- application을 loading 할 때, 생성된다는 점이 단점이다.
- 상속을 쓰지 못하는 것도 단점이다.
public class App {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Tools tools1 = Tools.INSTANCE;
Tools tools2 = null;
try (ObjectOutput output = new ObjectOutputStream(new FileOutputStream("tools.obj"))) {
output.writeObject(tools1);
}
try (ObjectInput input = new ObjectInputStream(new FileInputStream("tools.obj"))) {
tools2 = (Tools) input.readObject();
}
System.out.println(tools1 == tools2);
}
}
- enum은 enum이란 class를 상속받고 있는데, 이미 직렬화를 구현하고 있어서 직렬화와 역직렬화에 안전하다.
실제로 어디서 쓰일까
- spring bean의 scope 중에 하나로 singleton scope ( 실제로 singleton pattern은 아니다 )
public class SpringExample {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
String example1 = applicationContext.getBean("example", String.class);
String example2 = applicationContext.getBean("example", String.class);
System.out.println(example1 == example2);
}
}
Runtime runtime = Runtime.getRuntime();
- 다른 design pattern(builder, facade, abstract factory, etc) 구현체의 일부로 쓰이기도 한다.
출처