JAVA/Effective Java

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

꾸준함. 2024. 1. 19. 10:42

싱글톤(singleton)이란?

아이템 3에 대해 논의하기에 앞서 우선 싱글톤 개념에 대한 이해가 선행되어야 합니다.

간단히 요약하자면 싱글톤 객체는 아래의 특성을 가집니다.

  • 클래스의 인스턴스가 오직 1개만 생성되는 것을 보장하는 디자인 패턴 
  • connection pool, thread pool, 디바이스 설정 객체의 경우 인스턴스를 여러 개 생성하면 자원을 낭비하거나 버그를 야기할 수 있으므로 오직 하나만 생성하고 해당 인스턴스를 사용하도록 하는 것이 목적

 

보다 자세한 내용은 제가 과거에 작성한 게시글 및 Jbee님이 작성한 글을 참고 바랍니다.

 

https://jaimemin.tistory.com/1756

 

싱글톤(Singleton) 패턴

싱글톤 패턴 클래스의 인스턴스가 오직 1개만 생성되는 것을 보장하는 디자인 패턴 private 생성자를 통해 외부에서 임의로 new 키워드를 통해 객체 인스턴스를 사용하지 못하도록 방지 getInstance()

jaimemin.tistory.com

https://asfirstalways.tistory.com/335

 

[DP] 1. 싱글턴 패턴(Singleton pattern)

#1. 싱글턴 패턴(Singleton Pattern) 싱글턴 패턴이란 인스턴스를 하나만 만들어 사용하기 위한 패턴이다. 커넥션 풀, 스레드 풀, 디바이스 설정 객체 등의 경우, 인스턴스를 여러 개 만들게 되면 자원

asfirstalways.tistory.com

 

싱글턴 패턴을 보장하는 방법

싱글턴 패턴을 보장하는 방법은 아래와 같이 세 가지가 있습니다. (사실 첫 번째, 두 번째 방법은 유사합니다.)

  • private 생성자 + public static final 필드
  • private 생성자 + 정적 팩토리 메서드
  • Enum 타입

 

방법 1. private 생성자 + public static final 필드

 

가장 간결하고 직관적이게 싱글턴 객체를 생성할 수 있는 기법입니다.

아래와 같이 private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 한 번만 호출되고 클라이언트는 유일한 인스턴스에 접근할 수 있는 수단인 멤버 필드를 참고하는 구조입니다.

 

 

 

위 방식은 간결한 방법인만큼 아래와 같은 단점 및 허점들이 있습니다.

  • 싱글톤을 사용하는 클라이언트를 테스트하기 어려워짐
  • reflection을 통해 private 생성자를 호출해 여러 인스턴스를 생성 가능
  • 역직렬화할 때 새로운 인스턴스 생성 가능

 

싱글톤을 사용하는 클라이언트를 테스트하기 어려워짐

엘비스가 내한하여 콘서트를 주최하는 상황을 가정하겠습니다.

이는 아래와 같이 클래스로 표현할 수 있을 것입니다.

 

 

 

콘서트를 주최하기 전 리허설은 필수이지만 매 리허설마다 엘비스를 초청하기에는 엘비스 몸값이 비싸 단가가 맞지 않을 것입니다.

이처럼 Concert 테스트 코드를 작성할 때 Elvis 의 인스턴스를 가져와서 매번 노래를 부르게 하면 리소스를 지속적으로 사용하여 낭비하게 될 것입니다.

좀 더 구체적으로 설명하자면 Elvis 객체의 sing 메서드가 외부 API를 호출하고 이를 테스트한다고 가정했을 때 외부 API는 호출 횟수에 비례해서 비용이 발생할 것이고 이를 테스트할 때마다 호출하면 안 될 것입니다.

따라서 Elvis 객체를 인터페이스화하여 MockElvis 인터페이스를 만들고 Elvis 인스턴스를 대체해서 Concert 테스트를 진행하도록 수정하면 실제 API 호출을 하지 않음에 따라 비용을 줄이면서 외부 API 호출이 정상적으로 호출되는지 확인할 수 있게 됩니다.

정리하자면 테스트할 때마다 싱글톤 인스턴스를 불러오는 것은 비싼 작업이기 때문에 테스트가 어려워집니다.

따라서 해당 객체를 인스턴스화하여 Mocking해서 테스트하는 방식을 고려해봄직 합니다.

위 내용을 코드로 표현하면 아래와 같습니다.

 

 

 

reflection을 통해 private 생성자를 호출해 여러 인스턴스 생성 가능

Reflection을 사용하여 getDeclaredConstructor() 메서드를 호출할 경우 접근 지시자와 상관없이 기본 생성자에 접근이 가능합니다.

이는 아래 코드를 통해 확인이 가능합니다.

 

 

이를 방지하기 위해서는 싱글톤 인스턴스를 생성한 이후에 기본 생성자를 접근한 경우 예외를 던지게 하면 됩니다.

아래와 같이 private 생성자를 접근할 때 UnsupportedOperationException을 던지게 할 경우 리플렉션을 통해 기본 생성자를 접근하려고 할 때 예외가 발생하는 것을 확인할 수 있습니다.

 

 

역직렬화할 때 새로운 인스턴스 생성 가능

아래처럼 클래스를 직렬화하고 역직렬화할 때마다 새로운 인스턴스가 생성이 됩니다.

 

 

이를 방지하기 위해서는 Elvis 클래스에 readResolve()를 정의하고 싱글톤 인스턴스를 반환하도록 처리를 해야 합니다.

이는 역직렬화할 때 readResolve() 메서드를 사용하기 때문에 위와 같이 처리하는데 한 가지 흥미로운 점은 메서드 오버라이딩이 아니라는 점입니다.

 

 

정리하자면 첫 번째 방법은 아래와 같은 장단점이 존재합니다.

 

장점

  • 간결하고 가독성이 좋음
    • 생성자가 private 이므로 인스턴스를 생성하지 못하도록 막았다는 것을 쉽게 파악 가능
    • static final로 필드를 선언할 경우 java document를 생성할 때 별도 필드로 보여주기 때문에 파악하기 쉬움

 

단점

  • 싱글톤을 사용하는 클라이언트를 테스트하기 어려워짐
  • reflection을 통해 private 생성자를 호출해 여러 인스턴스를 생성 가능
  • 역직렬화할 때 새로운 인스턴스 생성 가능

* 하지만 위 단점들은 앞에서도 정리했다시피 모두 해결책이 있음

 

방법 2. private 생성자 + 정적 팩토리 메서드

 

첫 번째 방법과 유사하지만 static final 멤버를 private 접근 지시자로 선언하고 public static 메서드를 통해 싱글톤 객체를 반환하는 방식입니다.

 

 

 

위 방식을 채택했을 때 장점은 아래와 같이 세 가지가 있습니다.

  • API를 바꾸지 않고도 싱글톤이 아니게 변경 가능
  • 정적 팩토리 메서드를 제너릭 싱글톤 팩토리 메서드로 만들 수 있음
  • 정적 팩토리 메서드 참조를 Supplier로 사용 가능

 

API를 바꾸지 않고도 싱글톤이 아니게 변경 가능

클라이언트 코드 변경 없이 해당 객체를 싱글톤으로 반환할지 새로운 인스턴스로 반환할지 변경이 가능하다는 점입니다.

마음이 바뀌었을 경우 단순히 getInstance() 메서드만 변경하면 됩니다.

 

 

 

정적 팩토리 메서드를 제너릭 싱글톤 팩토리 메서드로 만들 수 있음

클라이언트가 싱글톤 인스턴스를 직접 접근하지 않고 getInstance()와 같은 메서드를 통해 참조하기 때문에 아래와 같이 제너릭 한 타입을 사용 가능합니다.

getInstance() 메서드에도 제너릭 타입을 명시해야 하는 이유는 scope가 다르기 때문입니다.

  • Java에서는 정적 메서드 내에서 클래스 레벨의 제네릭 타입에 직접적으로 접근할 수 없습니다.
  • 정적 메서드에서 사용되는 제네릭 타입은 해당 메소드 자체에서 선언되어야 합니다.

 

 

 

정적 팩토리의 메서드 참조를 Supplier로 사용 가능

해당 내용을 이해하기 위해서는 Java 8부터 도입된 Supplier 인터페이스와 함수형 인터페이스에 대한 선수 지식이 필요합니다.

  • 함수형 인터페이스는 하나의 추상 메서드만을 가지며 람다 표현식이나 메서드 참조와 같은 함수형 프로그래밍의 특징을 활용 가능
  • Supplier 인터페이스는 함수형 프로그래밍 기능을 지원하기 위한 인터페이스 중 하나이며 해당 인터페이스는 매개변수를 받지 않고 값을 제공하는 역할을 수행
  • Supplier 인터페이스의 메서드는 get()이며 이 메서드를 구현하는 클래스 또는 람다 표현식은 매개변수 없이 값을 생성하고 반환

 

 

 

java 8 이상 버전부터 Supplier interface 만 만족하면 어떠한 메서드던 Supplier functional type으로 사용 가능합니다.

따라서 Supplier를 구현하지 않았지만 Elvis 클래스가 Supplier에 준하기 때문에 아래와 같이 코드 작성이 가능합니다.

  • Elvis::getInstance는 정확히 Supplier 인터페이스의 get 메서드와 시그니처가 일치

 

 

 

단점은 방법 1과 동일하고 해결법 또한 동일하므로 생략하겠습니다.

 

방법 3. Enum 타입

 

싱글톤 객체임을 보장하는 가장 간결한 방법이며 대부분의 상황에서는 원소가 하나뿐인 Enum 타입이 싱글톤을 만드는 가장 좋은 방법입니다.

해당 방법을 채택할 경우 별도 처리 없이 Reflection과 역직렬화에 안전하다는 것이 가장 큰 장점입니다.

실제로 target 폴더 내 컴파일된 Enum 클래스에서는 기본 생성자가 존재하는 것을 확인할 수 있지만 리플렉션을 통해 호출하려고 했을 때 예외가 발생하는 것을 확인할 수 있습니다.

 

 

 

역직렬화할 때도 기존처럼 readResolve() 메서드를 별도 선언하지 않더라도 싱글톤 인스턴스를 보장하는 것을 확인할 수 있습니다.

 

 

해당 방법의 유일한 단점이라면 싱글톤이 Enum 외의 클래스를 상속해야 할 경우 사용할 수 없다는 것입니다.

단, Enum 타입이 다른 인터페이스를 구현하도록 선언할 수는 있습니다.

 

참고

이펙티브 자바

이펙티브 자바 완벽 공략 1부 - 백기선 강사님 + 커뮤니티 질문글

반응형