본문 바로가기

Reading/Effective Java

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

싱글톤(Singleton): 인스턴스를 오직 하나만 생성하는 클래스


 

예: 함수와 같은 무상태(stateless) 객체나 설계상 유일해야하는 시스템 컴포넌트

 

문제점:

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • client가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 싱글톤 클래스는 테스트하기 어렵다. mock 구현으로 대체할 수 없기때문이다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다

 

 

public static final 필드 방식의 싱글톤


 

public class SingletonPublicStaticFinalField {
    public static final SingletonPublicStaticFinalField INSTANCE = new SingletonPublicStaticFinalField();
    private SingletonPublicStaticFinalField(){ }
}

 

private 생성자를 가지며 Singleton1.INSTANCE를 초기화할 때 딱 한 번만 호출된다.

하지만 리플렉션을 사용해서 private 생성자를 호출할 수 있다.

방어하기 위해 생성자를 수정해 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.

 

장점

(1) 해당 클래스가 싱글턴임이 API에 명백히 드러나고, public static field가 final이니 절대로 다른 객체를 참조할 수 없다.

(2) 간결하기 때문에, 스태틱 팩토리 메소드를 사용하는 방법에 비해 더 명확하고 간단하다.

 

 

아래는 리플렉션을 이용해 private 생성자를 호출한 예이다.

original끼리는 참조하는 인스턴스가 같지만, 리플렉션을 이용해 만든 인스턴스는 새롭게 만들어짐을 알 수 있다.

 

@Test
@DisplayName("public static final 필드는 리플렉션으로 private 생성자를 호출할 수 있다.")
public void singletonPublicStaticFinalFieldTest() throws Exception {
  SingletonPublicStaticFinalField singletonOriginal = SingletonPublicStaticFinalField.INSTANCE;
  SingletonPublicStaticFinalField singletonOriginal2 = SingletonPublicStaticFinalField.INSTANCE;

  Class<?> aClass = Class.forName("effective.item3.SingletonPublicStaticFinalField");
  Constructor<?> constructor = aClass.getDeclaredConstructor();
  constructor.setAccessible(true);
  SingletonPublicStaticFinalField singletonReflection = (SingletonPublicStaticFinalField) constructor.newInstance();

  assertSame(singletonOriginal, singletonOriginal2);
  assertNotSame(singletonOriginal, singletonReflection);
}

 

 

정적 팩토리 방식의 싱글톤


 

public class SingletonStaticFactoryMethod {
    private static final SingletonStaticFactoryMethod INSTANCE = new SingletonStaticFactoryMethod();
    private SingletonStaticFactoryMethod(){ }
    public static SingletonStaticFactoryMethod getInstance(){ return INSTANCE; }
}

 

SingletonStaticFactoryMethod getInstance()는 항상 같은 객체의 참조를 반환하여 제 2의 인스턴스가 만들어지지 않는다.

 

장점

(1) API를 바꾸지 않고도 싱글톤으로 쓸지 말지 변경할 수 있다. 처음에 싱글톤으로 쓰다가 나중에 쓰레드별로 다른 인스턴스를 반환하는 등 클라이언트 코드를 고치지않고 사용할 수 있다.(getInstance()의 내부의 변경)

(2) 원한다면 Generic 싱글톤 팩토리를 만들 수 있다.(아이템30)

(3) 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다. 예를 들어 SingletonStaticFactroyMethod::getInstanceSupplier<SingletonStaticFactroyMethod>로 사용하는 식이다(아이템 43,44)

--> 간략하게 메서드를 하나만 가지고 있는 인터페이스고, get()하나있고 getInstance가 여기 사용될 수 있다는 것이다.

 

Supplier<SingletonStaticFactoryMethod> singletonSupplier = SingletonStaticFactoryMethod::getInstance;

 

Supplier는 내부 메소드를 T get() 하나로, T 객체를 가져온다.

 

 

정적 팩토리 방식의 싱글톤도 리플렉션을 통한 예외가 생기기 때문에 방어해줘야한다.

 

    @Test
    @DisplayName("static factory method 방식은 리플렉션으로 private 생성자를 호출할 수 있다.")
    public void singletonStaticFactoryMethodTest() throws Exception {
        SingletonStaticFactoryMethod singletonOriginal = SingletonStaticFactoryMethod.getInstance();
        SingletonStaticFactoryMethod singletonOriginal2 = SingletonStaticFactoryMethod.getInstance();

        Class<?> aClass = Class.forName("effective.item3.SingletonStaticFactoryMethod");
        Constructor<?> constructor = aClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonStaticFactoryMethod singletonReflection = (SingletonStaticFactoryMethod) constructor.newInstance();

        assertSame(singletonOriginal, singletonOriginal2);
        assertNotSame(singletonOriginal, singletonReflection);
    }

 

 

직렬화(Serialization)


자바 라이브러리를 향해 들어올 수 있는 공격은 역직렬화가 있다. 

위에서 살펴본 두 방식 모두 직렬화에 사용한다면, 역직렬화를 할때마다 새로운 인스턴스가 만들어진다.

직렬화된 객체를 역직렬화 하면서 필드를 다시 만들때 실행하며 타입의 인스턴스가 여러개가 될 수 있다는 뜻이다.

단순히 Serializable을 구현한다고 선언하는 것만으로 부족하고 모든 인스턴스 필드에 transient(일시적)를 선언하고

readResolve 메소드를 다음과 같이 구현하면 된다.(챕터 12 참고) 이 메서드가 있으면 항상 호출되기 때문에

INSTANCE를 바로 반환하게 하는 거고, 직렬화하지 않는다는 transient는 별개로 작용하는 것으로 보인다.

직렬화를 하지않는다는 transient만 선언한다고 해도 선언된 필드의 값만 보내지 않는 것이지,

역직렬화를 할 때 readResolve가 없으면 새로운 인스턴스가 생기기 때문이다.

 

오브젝트<--> xml,json 간에도 역직렬화 하는거고 자바의 Serializables는 조금 다른 것 같다.

참고: https://www.oracle.com/technical-resources/articles/java/serializationapi.html

(메모리 캐시, 네트워크 통신, 객체를 옮길수 있는 상태를 만들고 풀고 하는 그런 과정 ....들...? 나중에 다시보기 ㅠ ~ )

// 싱글턴임을 보장해주는 메서드
private Object readResolve(){
	// 진짜 인스턴스를 반환하고 가짜 인스턴스는 가비지 컬렉터에 맡긴다.
	return INSTANCE;
}

 

 

열거 타입 방식의 싱글톤


 

public enum SingletonEnum{
	INSTANCE;
}

 

간결하고 직렬화/역직렬화 상황이나 리플렉션 공격에서도 제 2의 인스턴스가 생기는 일을 막아준다. 필드들도 선언해서 사용할 수 있고.

하지만 이 방법은 Enum 말고 다른 상위 클래스를 상속해야 한다면 사용할 수 없다.

(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다) 

 

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

하지만 유연성이 없어보이기도하고 부자연스럽게 보이긴한다.스프링을 안쓴다면 이게 나을지도

 

 

스프링에서의 싱글톤


스프링 컨테이너에서 Repository, Service 등을 사용할 때 Component(service, controller, repository 포함) 기본적으로 싱글톤이 보장된다. 만약 ApplicationContext를 사용한다면 여기서 꺼내쓰는 Bean들은 싱글톤이 보장된다는 것이다.

repository, servcice 자체만으로는 싱글톤이 아니지만 스프링 컨테이너를 거친다면 싱글톤이 보장된다.

물론 Scope을 통해서 prototype을 통해서도 가능하다.

 

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.