티스토리 뷰
내가 좋아하는 싱글톤이다. 주의해서 사용한다면 메모리를 폭발적으로 아낄 수 있다.
책에서 싱글톤의 단점부터 소개한다.
클래스를 싱글톤으로 만들면 클라이언트에서 테스트 코드를 작성하기 어렵다고 한다.
일단 이건 무슨 말인지 모르겠으니 그렇구나 하고 넘어가기로 하자.
싱글톤을 만드는 두 가지 방법 중 첫 번째를 소개한다.
private static final 필드를 사용하고 생성자를 private으로 감춘다.
package effectivejava.chapter2.item3.field;
// 코드 3-1 public static final 필드 방식의 싱글턴 (23쪽)
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
의문점?
내가 싱글톤에 대해 배울 때 thread safe를 고려하며 싱글톤을 구현하기 위해 다양한 기법들을 사용했었다.
double checked locking과 같은 친구를 구현하기 위해 코드를 되게 길게 썼었다.
근데 이건 왜 이렇게 짧지?..
성서 이펙티브 자바를 의심하는 것은 아니지만 그 차이를 찾아보기로 했다.
다행히도 스택 오버플로우에 나랑 똑같은 생각을 한 사람이 있었다.
private static MyClass instance = null;
public static MyClass getInstance(){
if (instance == null){
instance = new MyClass();
}
return instance;
}
private MyClass(){}
------------------------------------------------------------------------
public static final MyClass instance = new MyClass();
private MyClass(){}
이거 두 개 차이가 무엇이냐는 질문이었다.
우선 두 코드의 차이점은 아래의 코드는 final field를 사용한다는 것이다.
위의 final을 사용하지 않는 코드는 MyClass의 instance를 처음 호출 할 때 인스턴스가 생성되게 된다.
그러나 위의 코드는 thread safe 하지 않다. multi thread 환경에서 thread1이 if문을 통과해서 인스턴스를 생성하려는 찰나
스위칭이 발생해 다른 thread2가 인스턴스를 생성하고 다시 thread1의 차례가 왔을 때 새로운 인스턴스를 만들어버린다.
이를 해결하기 위해 막 락을 걸고 두 번 검사하고 그런 코드를 작성해야 한다..
아래의 코드는 프로그램 돌리자마자 JVM이 클래스들 살펴보면서 static final 친구들은 static 영역에 인스턴스 생성을 한다.
JVM이 자동으로 thread safe 하게 구현해주기에 아주 편하다.
엥? 그럼 아래의 코드를 쓰면 되는 거 아니냐라고 물을 수 있지만 MyClass라는 친구가 프로그램 내에서 사용되지 않게 되면 메모리 잡아먹는 금쪽이가 된다.
그래서 MyClass의 인스턴스를 호출했을 때 생성하는 방식에 대해 고민을 해봐야 한다.
이때 이 둘을 융합한 LazyHolder 패턴을 쓸 수 있다.
public class MyClass {
private MyClass() {
}
private static class LazyHolder {
public static final MyClass INSTANCE = new MyClass();
}
public static MyClass getInstance() {
return LazyHolder.INSTANCE;
}
}
클래스 안에 static inner class를 만들어 둔다. JVM 입장에서 LazyHolder는 final이 아니니까 미리 만들어 둘 필요가 없다.
getInstance가 호출되면 그제야 인스턴스를 생성해 이를 반환해 준다.
thread safe 대한 고민을 JVM이 하게 하고 클래스가 호출될 때 인스턴스가 생성된다. (우왕 개쩐다 ㅋㅋ)
LazyHolder가 잘 이해가 안 됐는데 좀 이해가 된 거 같다.
책에서 두 번째 싱글턴 방식을 소개한다.
package effectivejava.chapter2.item3.staticfactory;
// 코드 3-2 정적 팩터리 방식의 싱글턴 (24쪽)
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
아이템 1의 static 팩토리 메소드로 인스턴스를 제공하는 것이다.
둘 다 리플렉션을 사용하면 싱글톤을 깨버릴 수 있다.
리플렉션을 쓰면 private 생성자를 바깥에서 호출할 수 있기 때문이다.
코드 3-1의 장점은 해당 인스턴스가 싱글톤임을 명확하게 알 수 있다는 점이다.
인스턴스가 public static final이어서 다른 레퍼런스를 참조할 수 없다. 그리고 간결하다.
코드 3-2의 첫 번째 장점은 static 팩토리 메소드를 클라이언트 코드를 고치지 않고 싱글톤이었는데 싱글톤 아니게도 바꿀 수 있다.
method를 통해 instance를 호출하고 있었기 때문에 method 내부를 변경하면 다르게 사용할 수 있다.
두 번째 장점은 제네릭 싱글톤으로 구현할 수 있다. method 제네릭으로 구현하면 유연한 객체 반환이 가능하다.
세 번째 장점은 Elvis::getInstance를 Supplier<Elvis>로 사용할 수 있게 된다.
Supplier는 표준 functional Interface로 매개변수를 받지 않고 반환값을 제공한다.
Supplier를 람다식으로 사용하는 메소드에 쓸 수 있다.
싱글톤 클래스의 직렬화
싱글톤으로 설계된 클래스를 직렬화하기 위해서는 implements Serializable만으로는 부족하다.
이해가 안 돼 미래에서 12장을 읽고 왔다.
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since JDK1.1
*/
public interface Serializable {
}
Serializable을 구현해야 할 method가 아무것도 없다.
그러나 이 친구는 숨겨진 클래스가 있다. Serializable을 implements 한 클래스는 숨겨진 클래스의 메소드인
writeObject와 readObject를 통해 직렬화와 역직렬화를 진행하게 된다.
writeObject는 기본적으로 transient 지정자가 붙어 있지 않은 필드들을 모아서 바이트 코드로 변환한다.
변환한 바이트 코드를 readObject 메소드를 통해 해당 클래스와 똑같이 생긴 "복사본" 클래스를 재구성 할 수 있다.
이는 싱글톤의 인스턴스 단일 생성과 맞지 않다.
이를 해결하기 위해서 모든 인스턴스 필드를 transient로 지정하고 private readResolve 메소드를 제공하면 해결할 수 있다.
private Object readResolve() {
// 진짜 인스턴스를 반환하고, 가짜는 가비지컬렉터에 의해 처리된다 ㅋ.ㅋ
return INSTANCE;
}
아니 Serializable은 method가 아무것도 없잖아요!!
숨겨져 있는 클래스에 의해 readResolve 함수를 reflection을 통해 감지해 재정의 된다고 한다.
이때 지정자는 private이어야 감지가 된다. readObject와 writeObject 또한 같은 방법으로 재정의 할 수 있다.
"모든 인스턴스 필드를 transient로 지정" 이건 왜 하는 걸까
직렬화를 진행할 때 Elivs의 필드 값을 저장할 필요가 없기 때문이다.
readObject를 진행할 때 아무것도 안 들어 있는 애를 직렬화하고, writeObject를 통해 이를 역직렬화한다.
이때 readResolve에 의해 원본 인스턴스를 리턴하기 때문에 필드 정보를 담을 필요가 없다.
(근데 이럴 거면 직렬화를 왜 하지?..)
또 무슨 바이트 코드 조작하는 공격에도 대비할 수 있다고 한다.
싱글톤을 만드는 세 번째 방법은 다음과 같다.
package effectivejava.chapter2.item3.enumtype;
// 열거 타입 방식의 싱글턴 - 바람직한 방법 (25쪽)
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("기다려 자기야, 지금 나갈께!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
이게 뭐지?.. 정신이 아득해진다. enum을 잘 몰라서 enum부터 공부하고 왔다.
enum은 상수를 열거하는 클래스이다. 또한 인스턴스와 메소드를 가질 수 있고, private 생성자를 사용한다.
enum 생성 시점은 JVM이 컴파일할 때이다. 상수 값이라 컴파일 시점에 모든 값들이 지정돼 있어야 한다.
private static final과 비슷하지만 enum의 장점들이 존재한다.
enum 클래스의 직렬화는 JVM이 알아서 해준다.(오오 ㅋㅋ) 무언가 우리가 고민해야 하는 부분이 없다.
리플렉션으로 enum 클래스의 생성자를 호출할 수 없다. 따라서 리플렉션을 통해 싱글톤을 깰 수 없다.
단점으로는 enum은 클래스를 상속받을 수 없다. Interface는 implements 할 수 있다.
클래스를 상속받는 일이 없다면 이 방법이 최고라고 한다.
느낀 점 : 아이템 3은 3페이지이다. 근데 이걸 이해하려면 읽어야 되는 부분이 너무 많다.
조금 화가 나지만 많을 것을 배운 것 같다 ^^7
코드 출처 :
싱글톤 의문 :
https://stackoverflow.com/questions/33475602/singleton-public-static-final
직렬화:
https://madplay.github.io/post/what-is-readobject-method-and-writeobject-method
이놈:
https://www.nextree.co.kr/p11686/
'Book > Effective Java' 카테고리의 다른 글
Effective Java - Item 6. 불필요한 객체 생성을 피하라 (0) | 2023.01.05 |
---|---|
Effective Java - Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.01.03 |
Effective Java - Item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2023.01.02 |
Effective Java - Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.01.02 |
Effective Java - Item 1. 생성자 대신 static 팩토리 메소드를 고려해 볼 것 (0) | 2022.12.24 |