Study/이펙티브 자바 / / 2023. 8. 13. 04:20

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

💥 개요

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다.

싱글턴의 전형적인 예시로 함수와 같은 무상태성(stateless) 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있습니다. 그런데 클래스를 싱글턴으로 만들면 사용하는 클라이언트를 테스트하기가 어려워 질 수 있습니다. 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 mocking 할 수 없기 때문입니다.

 

💡싱글턴을 만드는 방식

우선 생성자를 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둡니다.

1. public static 멤버가 final 필드인 방식

private 생성자는 public static 필드인 INSTANCE를 초기화할 때 딱 한번만 호출됩니다. public이나 protected 생성자가 없음으로 Elvis 클래스가 초기화 될 때 만들어진 인스턴스가 싱글턴임을 보장합니다. 이는 클라이언트가 손 쓸 방법이 없습니다. 예외는 리플렉션을 이용해 AccessibleObject.setAccessible을 사용해서 private 생성자를 호출할 수 있습니다. 

이러한 공격을 방어하려면 생성자를 수정해 두번 째 객체가 생성되려 할 때 예외를 던지는 방법이 있습니다.

첫번째 방식의 큰 장점은 해당 클래스가 싱글턴임이 API에 드러나는 것 입니다. public static 필드가 final이니 절대 다른 객체를 참조할 수 없습니다. 두 번째 장점은 간결함입니다.

 

2. 정적 팩터리 메서드를 public static 멤버로 제공

여기서 getInstance는 항상 같은 객체의 참조를 반환하므로 제2의 인스턴스란 결코 만들어지지 않습니다.(리플렉션을 통한 예외는 똑같이 적용)

또한 API를 변경하지 않고도 싱글턴이 아니게 변경할 수 있다는 점 입니다. 유일한 인스턴스를 반환하던 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨 줄 수 있게 변경이 가능합니다.

세 번째 장점은 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다는 점 입니다.

가령  StaticFactory::get Instance를 Supplier<StaticFactory>로 사용하는 방식으로 이런 장점들이 필요하지 않다면 public 필드 방식이 더 좋습니다.

둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화(serialize) 하려면 단순히 Serializable을 구현한다고 선언하는것으로는 부족합니다. 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 합니다. 이렇게 하지 않으면 serialize된 인스턴스를 deserialize할 때 마다 새로운 인스턴스가 생성됩니다. 즉 가짜 객체가 탄생하게 되기 때문에 이를 예방하려면 readResolve 메서드를 추가해야 합니다.

 

3. 열거 타입을 선언하는 방식 (Best)

public 필드 방식과 비슷하지만, 더 간결하고, 직렬화하기 쉽습니다. 심지어 아주 복작한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아줍니다. 조금 부자연스러워 보이지만 대부분 상황에서 원소가 하나뿐인 Enum 타입이 싱글턴을 만드는 가장 좋은 방법입니다. 단 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없습니다.(열거 타입이 다른 인터페이스를 구현하도록 선언은 가능합니다.)

 

Github (Source Code)

https://github.com/devMtn30/Effective_Java_3E/tree/master/src/main/java/chap02/item3

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유