본문 바로가기

책, 강의 정리/이펙티브자바

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

싱글턴

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

ex) 함수

 

싱글턴의 한계?

싱글턴을 사용하는 클라이언트 테스트가 어렵다.

 

 

 

싱글턴을 만드는 방법

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;

  assertThat(foo1).isEqualTo(foo2); // success 
}

 

장점: 간결하고 API 에 싱글턴임이 명백히 드러남(명확하게 이해 안됨)

 

 

예외) 리플렉션 API

     Constructor<Foo> constructor = (Constructor<Foo>) foo2.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);
        Foo foo3 = constructor.newInstance();
        assertThat(foo1).isEqualTo(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();

        assertThat(bar1).isEqualTo(bar2);//success
    }

 

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

 

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

 

 

 

두 방식의 문제점

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

 

 

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

public enum Singleton{
  INSTANCE;
}

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