이 글도 이전에 작성했던 싱글톤 패턴에 대한 내용의 반복인데, 책을 읽으며 부족한 부분을 채울 수 있게되어 간단하게나마 다시 작성하게 되었다.
1
2
3
4
5
6
7
8
9
10
11
12
|
public class BasicSingleton {
public static final BasicSingleton INSTANCE = new BasicSingleton();
private BasicSingleton() {
}
public static void main(String[] args) {
BasicSingleton instance = BasicSingleton.INSTANCE;
}
}
|
cs |
이게 우리가 알고 있는 가장 보편적인 싱글톤의 형태이다.
이 형태는 간결하고 싱글톤임을 쉽게 드러낼 수 있다는 장점이 있다.
(소스 코드를 종종 열어보면서 지금까지 본 대다수의 싱글톤이 이렇게 구현되어 있었다.)
하지만 이 상태에서는 완벽한 싱글톤이 보장되지 않는다. 왜?
Reflection API와 Serializable로 싱글톤을 깨트릴 수 있기 때문이다.
우선 Reflection API.. 에 대해 얘기하기 이전에.
https://7357.tistory.com/194?category=1060668
https://7357.tistory.com/195?category=1077082
Reflection API를 모른다면 위의 글을 참고하자. 두 번 쓰기는 힘들다..
책에서 저자가 소개하고 있는 Reflection API를 막는 방법은 생성자에 조건문을 만들어주는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class BasicPlusSingleton {
public static final BasicPlusSingleton INSTANCE = new BasicPlusSingleton();
private static boolean created;
private BasicPlusSingleton() {
if (created) {
throw new RuntimeException();
}
created = true;
}
private Object readResolve() {
return INSTANCE;
}
}
|
cs |
이렇게 생성자에서 최초 생성 시 boolean 값을 true로 전환하고 이후 생성자에 접근하는 것 자체를 막아버린다면 Reflection API도 손 쓸 방법이 없다.
기본 생성자 자체를 원천 봉쇄하면 Reflection API로 아무것도 할 수 없다는 사실은 JPA 엔티티에 항상 @NoArgsConstructor를 붙여줘야하는 이유를 궁금해했던 사람이라면 다들 알고 있을 것이다.
그러면 serialization, de-serialization 시 싱글톤이 망가지는 것은 어떻게 막을 수 있을까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class BasicPlusSingleton {
public static final BasicPlusSingleton INSTANCE = new BasicPlusSingleton();
private static boolean created;
private BasicPlusSingleton() {
if (created) {
throw new RuntimeException();
}
created = true;
}
private Object readResolve() {
return INSTANCE;
}
}
|
cs |
그것은 바로 readResolve()라는 메서드에서 미리 만들어놓은 정적 객체를 리턴하도록 하는 것이다.
이렇게하면 역직렬화 시 무엇을 리턴할지 지정해줄 수 있다.
근데 이건 뭐 Override도 아니고 대체 어디서 튀어나온 건지.. 단순히 이 메서드 하나 만들었다고 그렇게 된다는게 의아해서 열심히 찾아봤다.
음.. 나의 부족한 독해 실력과 부족한 자바 지식이 겹쳐져 모든 말이 이해되지는 않지만 중요한 부분들만 읽어보자.
For Serializable and Externalizable classes, the readResolve method allows a class to replace/resolve the object read from the stream before it is returned to the caller.
By implementing the readResolve method, a class can directly control the types and instances of its own instances being deserialized.
...
...
For example, a Symbol class could be created for which only a single instance of each symbol binding existed within a virtual machine.
The readResolve method would be implemented to determine if that symbol was already defined and substitute the preexisting equivalent Symbol object to maintain the identity constraint.
In this way the uniqueness of Symbol objects can be maintained across serialization.
이제 이해는 되는데, 여전히 오버라이드도 뭣도 없이 그냥 해당 메서드가 구현되어 있으면 ObjectInputStream이 알아서 체크한다는 건 좀 신기하면서도 찝찝하기는 하다.
아무튼 해당 메서드를 구현해놓고 생성자도 막아놓으면 Serialization과정과 Refection으로 인해 싱글톤이 망가지는 것을 모두 방지할 수 있다.
여기까지가 가장 기본적인 형태의 싱글톤에 대한 내용이였고, 두 번째는 팩터리 메서드를 이용한 싱글톤 구현으로 이것도 소스코드에서 자주 볼 수 있는 형태이다.
주로 getInstance()라는 이름으로 많이 사용되고 있는 것 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class SecondSingleton<T> implements Parent {
private static final SecondSingleton INSTANCE = new SecondSingleton();
private SecondSingleton() {}
public static <T> SecondSingleton<T> getInstance() {return
(SecondSingleton<T>) INSTANCE;}
public T temp(T t) {
return t;
}
@Override
public void print() {
System.out.println("print");
}
}
|
cs |
당연한 얘기지만 리플렉션이나 직렬화로 싱글톤이 깨지는 문제는 똑같다.
다만 메서드를 이용해서 인스턴스를 리턴하기 때문에 조금이나마 확장성을 더 챙길 수 있으며, Generic을 이용할 수 있다는 게 큰 차이점이다.
비록 싱글톤은 아니지만 비슷한 맥락으로 봤을 때 아래와 같은 형태로 사용한다는 것이다.
또 하나의 사소한 차이를 보자면, 해당 방법을 이용해서 이너 클래스에 인스턴스를 만들어놓고 필요할 때 생성하는 방법도 사용할 수 있다.
위의 방법과 이 방법의 차이를 모르겠다면 클래스 로딩 및 초기화 시점에 대한 공부가 필요하다.
당연히 나도 잘 아는 것은 아니고.. 추상적으로라도 알고 있으면 좋으니 시간 날 때 정독해보는 것도 괜찮을 것 같다.
마지막 방법은 Enum으로 구현하는 싱글톤이다.
1
2
3
4
5
6
7
8
9
10
11
|
public enum LastSingleton {
INSTANCE;
public void print() {
System.out.println("print");
}
public static void main (String[] args) {
LastSingleton instance = LastSingleton.INSTANCE;
}
}
|
cs |
이 방법은 serialization에도 Reflection API의 공격에도 끄덕 없으며, 심지어 아무런 코드를 추가하지 않아도 된다.
1. Enum도 사실은 클래스이기 때문에 보이지 않는 생성자를 가지고 있으나, 아무튼 기본 생성자를 사용하는 것이 원천봉쇄되어 있어서 Reflection API로 복제할 수 없다.
2. Enum은 serialization시 전체 코드를 직렬화하는 것이 아니라 참조만 직렬화하는 것이라고 한다. (이 부분은 API 문서가 아닌 StackOverFlow에서 확인한 것이므로 검증이 필요함) 따라서 당연히 역직렬화 시에도 기존의 인스턴스를 반환하게 된다.
음.. 그러나 나는 좀 거부감이 든다. 실제로 저자조차 " 이 방법은 조금 이상해보일 수 있지만.. "이라고 표현했다..
또한 enum은 상속이 불가능하므로 추후 상속할 수도 없다.
그렇다, 그냥 enum으로 싱글톤을 만든다는 게 마음에 들지 않아서 트집 잡아본 것이다.
아무튼 이렇게 오늘 분량도 끝이다.
'Language & Framework > Java' 카테고리의 다른 글
이펙티브 자바 (5) 불필요한 객체 생성을 피하라 (feat. StringPool, Autoboxing) (0) | 2022.12.13 |
---|---|
이펙티브 자바 (4) 자원을 직접 명시하지 말고 의존 객체를 주입해서 사용하라 (+ item4) (0) | 2022.12.02 |
이펙티브 자바 (2) 생성자에 매개 변수가 많으면 빌더를 고려하라 (+ 자바에서 freeze 구현하기) (0) | 2022.11.12 |
이펙티브 자바 (1) 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.11.01 |
대혼돈의 질서 파괴범 Reflection API에 대해 알아보자 (Java) (2) | 2022.09.11 |