포디 Podi
또렷한 기억보다 글이 낫다
포디 Podi
  • 분류 전체보기 (83)
    • 책 (14)
      • 기록 (14)
      • 한줄 (0)
    • IT (62)
      • 기록 (61)
    • 일상 (5)
      • 음식 (0)
      • 기록 (5)
    • 게임 (0)
      • 몬스터헌터 (0)

블로그 메뉴

  • 홈으로
  • 방명록
  • 태그들
  • 깃허브
  • 유튜브
  • 인스타

공지사항

인기 글

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
포디 Podi

또렷한 기억보다 글이 낫다

[Effective Java] 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라
책/기록

[Effective Java] 아이템3 private 생성자나 열거 타입으로 싱글턴임을 보증하라

2022. 6. 20. 00:03
728x90

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
    '책/기록' 카테고리의 다른 글
    • [Effective Java] 아이템5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
    • [Effective Java] 아이템4 인스턴스화를 막으려거든 private 생성자를 사용하라
    • [Effective Java] 아이템2 생성자에 매개변수가 많다면 빌더를 고려하라
    • [Effective Java] 아이템1 생성자 대신 정적 팩토리 메서드를 고려하라
    포디 Podi
    포디 Podi
    기록은 복리다

    티스토리툴바