본문 바로가기

Reading Record/이펙티브자바

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

싱글턴

인스턴스를 오직 하나만 생성할 수 있는 클래스

ex) 함수

 

싱글턴의 한계?

싱글턴을 사용하는 클라이언트 테스트가 어렵다. 하나의 자원을 공유하기 때문에 테스트로 써도 되는 클래스인가를 판별해야 하는 것 같다. 예를들어, 실 DB에 접근하는 싱글턴을 테스트에 적용하면 안된다. 

 

싱글턴을 만드는 방법

1. 싱글턴 인스턴스를 public static final 필드로 만들고 생성자를 private 으로 한다.

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

@Test
public void 싱글턴테스트(){
    Foo foo1 = Foo.INSTANCE;
    Foo foo2 = Foo.INSTANCE;

    assertSame(foo, foo2); // success 
}

장점: 간결하다, 누가봐도 싱글턴 클래스임을 알 수 있다.

예외) 리플렉션 API를 사용하면 새로운 인스턴스를 생성할 수 있다.

     Constructor<Foo> constructor = (Constructor<Foo>) foo2.getClass().getDeclaredConstructor();
     constructor.setAccessible(true);
     Foo foo3 = constructor.newInstance();
     assertNotSame(foo2, foo3); // fail 

 

해결방법: 생성자에 검증작업을 추가하여 새로운 인스턴스를 생성하지 못하도록 한다.

 private Foo() {
        if(INSTANCE != null){
            throw new RuntimeException("생성자를 호출할 수 없습니다!!");
        }
    }

 

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

public class Bar {
    private static final Bar INSTANCE = new Bar();
    private Bar(){
        if(INSTANCE != null){
            throw new RuntimeException("싱글턴을 깨지 마세요!");
        }
    }
    public static Bar getInstance(){
        return INSTANCE;
    }
}

  @Test
    public void getInstance() {
        Bar bar1 = Bar.getInstance();
        Bar bar2 = Bar.getInstance();

        assertSame(bar1, bar2);//success
    }

 

장점: 팩터리메서드만 수정하면 언제든지 싱글톤이 아니게 바꿀 수 있으며, 제네릭 싱글턴 팩터리로 만듦으로써 타입에 유연하게 대처할 수 있다. 또, 공급자(Supplier)로 만들 수 있다.

Supplier<Bar> barSupplier = Bar::getInstance;
Bar bar = barSupplier.get();

 

두 방식의 문제점

각 클래스를 직렬화한 후 역직렬화할 때 새로운 인스턴스를 만들어서 반환한다. 이를 방지하기 위해 readResolve() 에서 싱글턴 인스턴스를 반환하고, 모든 필드에 transient(직렬화 제외) 키워드를 넣는다.

역직렬화는 기본 생성자를 호출하지 않고 값을 복사해서 새로운 인스턴스를 반환한다. 그때 통하는게 readResolve() 메서드이다. 

SingletonObject 는 기본 생성자 호출 시 메시지를 출력한다. 하지만 위 테스트코드를 돌려보면 기본 생성자에서 호출하는 메시지는 처음 singletonObject 를 만들 때만 호출되고 역직렬화시는 호출되지 않는다.

만약 SingletonObject 클래스의

이 부분을 주석처리 한다면 테스트코드의 assertSame 문은 실패한다. 즉, 역직렬화 시 readResolve 메서드가 싱글턴 객체를 반환하도록 하지 않으면 새로운 인스턴스를 반환한다는 의미이다. 신기하게도 기본 생성자는 호출되지 않고서 말이다.

 

+ 아직 해결되지 않은 궁금증 하나는 transient 키워드에 대한 것이다. transient 키워드는 붙이든 붙이지 않든 readResolve 에서 싱글턴 인스턴스를 반환하도록만 하면 해당 테스트코드는 성공한다. 그럼 transient 키워드는 왜 붙이라고 했을까 ..? 

 

3. 원소가 하나인 enum 타입 선언

public enum Singleton{
	INSTANCE; 
}

대부분의 상황에서 가장 좋은 방식. (기본적으로 직렬화되어있음)