https://book.naver.com/bookdb/book_detail.nhn?bid=14097515
이펙티브 자바
자바 플랫폼 모범 사례 완벽 가이드 - JAVA 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브
book.naver.com
이 글은 이펙티브 자바 Effective Java를 읽고 희미한 기억을 또렷한 기록으로 바꾸기 위해 작성했습니다
들어가기 전에
싱글턴(singleton)
인스턴스를 오직 하나만 생성할 수 있는 클래스
싱글턴의 전형적인 예로는 함수(static 메소드)와 같은 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트 등
클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워질 수 있다.
타입을 인터페이스로 정의한 다음 인터페이스를 구현해서 만든 싱글턴이 아니라면 mock구현으로 대체할 수 없어서 테스트가 어렵다
싱글턴 만드는 방식
public static final 필드 방식의 싱글턴
public이나 protected생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 하나뿐임이 보장
private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 1번 호출
이처럼, 일반 클라이언트는 손 쓸 방법이 없지만 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
공격을 방어하려면 생성자를 수정하여 2번째 객체가 생성되려 할 때 예외를 던지게 해야 한다.
정적 팩토리 방식
정적 팩토리 메소드를 public static 멤버로 제공한다.
public 필드 방식의 장점
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다는 것
public static필드가 final이니 절대로 다른 객체를 참조할 수 없음 - 간결함
정적 팩토리 방식의 장점
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다
유일한 인스턴스를 반환하던 팩토리 메소드가 호출하는 스레드 별로 다른 인스턴스를 넘기기 - 원한다면 정적 팩토리를 제너릭 싱글턴 팩토리로 만들 수 있다
- 메소드 참조를 공급자(supplier)로 사용할 수 있다
-> Elvis2::getInstance를 Supplier<Elvis2>
싱글턴 클래스를 직렬화
둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화 하려면 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다
모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메소드를 제공해야 한다
이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화 할 때마다 새로운 인스턴스가 만들어진다
열거 타입 방식의 싱글턴
public 필드 방식과 비슷하지만, 더 간결하다.
동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제 등을 enum으로 싱글턴을 생성하면 해결할 수 있다
대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.
실행해보기
문제 확인해보기
이 책에서는 첫 번째 방식과 두 번째 방식에서리플렉션 문제점과 직렬화 및 역직렬화에서 readResolve()을 추가하지 않으면 문제가 발생핬다고 나와있다!
리플렉션
첫번째 방식에 리플렉션 API을 사용해 생성자를 호출해보고 인스턴스 주소를 확인해보자!
//public static final 필드 방식의 싱글턴
Elvis elvis = Elvis.INSTANCE;
System.out.println(elvis);
//리플렉션 API
Constructor<?> con = Elvis.class.getDeclaredConstructors()[0];
con.setAccessible(true);
elvis = (Elvis) con.newInstance();
System.out.println(elvis);
결과는 아래와 같이 나왔다
effectiveJava.chapter01.item03.Elvis@36baf30c
effectiveJava.chapter01.item03.Elvis@7a81197d
이렇게 private 생성자를 호출해서 문제가 발생했다.
이 공격을 방어하려면 생성자를 수정해서 두 번째 객체가 생성되려 할 때 예외를 던지면 된다고 나와있다!
private Elvis() {
if(INSTANCE != null) {
//예외 처리
}
}
직렬화 및 역직렬화
readResolve 메소드를 제공하지 않으면 새로운 인스턴스가 만들어진다고 나와있다.
그러면 우선 메소드를 제공하지 않으면 정말 새로운 인스턴스가 만들어지는지 확인해보겠습니다
//정적 팩토리 방식의 싱글턴
Elvis2 elvis2 = Elvis2.getInstance();
System.out.println(elvis2);
//직렬화
byte[] serializedElvis2;
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try(ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(elvis2);
serializedElvis2 = baos.toByteArray();
}
}
//역직렬화
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedElvis2)) {
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
Object objectPost = ois.readObject();
elvis2 = (Elvis2) objectPost;
}
}
System.out.println(elvis2);
이렇게 해서 결과를 확인해보면!
effectiveJava.chapter01.item03.Elvis2@24d46ca6
effectiveJava.chapter01.item03.Elvis2@1698c449
오! 정말 새로운 인스턴스가 만들어졌습니다
Elvis2.java
private Object readResolve() {
return INSTANCE;
}
그럼 이번엔 readResolve 메소드를 제공해서 가짜는 GC에게 맡겨보겠습니다
effectiveJava.chapter01.item03.Elvis2@24d46ca6
effectiveJava.chapter01.item03.Elvis2@24d46ca6
오! GC가 잘 처리해줬네요
Enum
그러면 왜 Enum은 리플렉션, 직렬화 역직렬화 문제를 해결해준다는 걸까요?
java 11 docs에서 Enum을 확인해보겠습니다
리플렉션 문제를 해결할 수 있는 이유
enum의 생성자는 Sole Constructor이다.
Sole Constructor은 프로그래머가 생성자를 직접 호출할 수 없고, 컴파일러가 enum 응답하여 내보낸 코드에서 사용하기 위한 것이다.
그래서 권한이 있는 클라이언트는 리플렉션 API로 생성자를 호출 할 수 있는데
enum의 경우는 프로그래머가 생성자를 직접 호출 할 수 없기에 이 문제를 해결할 수 있다.
직렬화&역직렬화 문제를 해결할 수 있는 이유
Enum은 기본적으로 Serializable Interface가 구현되어 있어서 기본적으로 직렬화가 가능하다.
그렇기 때문에 역직렬화 시 새로운 객체가 생성될 걱정이 없으므로 직렬화&역직렬화 문제에서 해결해준다.
[참고]
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Enum.html
'책 > 기록' 카테고리의 다른 글
[Effective Java] 아이템5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.06.21 |
---|---|
[Effective Java] 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.06.20 |
[Effective Java] 아이템2 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2022.06.18 |
[Effective Java] 아이템1 생성자 대신 정적 팩토리 메서드를 고려하라 (0) | 2022.06.16 |
[프로그래머의 뇌] 변수의 역할 (0) | 2022.06.03 |