제육's 휘발성 코딩
article thumbnail
반응형

private 생성자나 열거 타입으로 싱글톤임을 보장하라

싱글톤은 인스턴스를 오직 하나만 생성할 수 있도록 보장해주는 디자인 패턴을 의미한다. 대표적인 예로는 stateless한 객체나 함수 상에 유일하게 설계하는 경우이다. 즉, 반대로 싱글톤 객체는 stateful하게 설계하면 안된다.

싱글톤 관련 포스팅 : https://sasca37.tistory.com/20

 

클래스를 싱글톤으로 설계하면 이를 사용하는 클라이언트를 테스트하기가 어려워 질 수 있다. 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글톤이 아니라면, 싱글톤 인스턴스를 mock 객체로 대체할 수 없기 때문이다.

싱글톤 방식은 public static final 방식과 정적 팩터리 방식, Enum 방식이 존재한다. 앞의 두 방식 모두 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 생성한다.

 

public static final 방식

package effective.item3;


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonService {

    public static final SingletonService INSTANCE = new SingletonService();

    private SingletonService(){}

    public void logic () {
        System.out.println("싱글톤 객체 호출");
    }
}
  • 장점
    • private 생성자를 통해 외부 호출을 막고, public static final 필드를 통해 딱 한번만 초기화하여 인스턴스를 공유한다. 즉, 클래스 초기화 시점에 생성된 인스턴스가 전체 시스템에서 하나뿐임을 보장한다.
    • 해당 클래스가 싱글톤임이 API에 명백하게 드러난다.

 

정적 팩터리 방식

public class SingletonService {

  private static final SingletonService instance = new SingletonService();

  public static SingletonService getInstance() {
    return instance;
  }

  private SingletonService(){}

  public void logic () {
    System.out.println("싱글톤 객체 호출");
  }
}
  • 장점
    • API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다. 즉, 스레드 별로 다른 인스턴스를 넘겨주게 할 수 있다.
    • 정적 팩터리를 제네릭으로 만들 수 있다.
    • 정적 팩터리의 메서드 참조를 supplier로 사용할 수 있다.
  • 해당 장점을 사용하지 않는다면 public static final 방식이 바람직하다.
class Test {
  public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Supplier<SingletonService> sss = SingletonService::getInstance;
    SingletonService ss = sss.get();
    ss.logic();
  }
}
  • 다음과 같이 supplier를 통한 인스턴스 호출 방식의 예이다. Consumer, Function, Operator, Predicate 등 다양한 공급자가 존재한다.

 

리플렉션 문제

private 한 생성자로 싱글톤을 보장해줄 수 있다. 하지만 예외적으로 리플렉션 API에서 private한 생성자에 접근할 수 있다.

리플렉션이란 구체적인 클래스 타입을 알지 못해도 컴파일된 바이트 코드를 통해 해당 클래스의 메서드, 타입, 변수까지 접근 가능하게 해주는 자바 API이다. 바이트 코드로 접근하기 때문에 static, private으로 선언된 부분도 접근할 수 있게 된다.

class Test {
  public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    SingletonService ss = SingletonService.INSTANCE;
    Constructor<SingletonService> cs = (Constructor<SingletonService>) ss.getClass().getDeclaredConstructor();
    cs.setAccessible(true);
    SingletonService ss2 = cs.newInstance();

    System.out.println(ss); // effective.item3.SingletonService@7dc36524
    System.out.println(ss2); // effective.item3.SingletonService@35bbe5e8
  }
}
  • 예외적으로 권한이 있는 클라이언트는 리플렉션 APIAccessibleObject.setAccessible 을 사용하여 private 생성자를 호출할 수 있다. 해당 예시를 보면 다른 인스턴스가 생성된 것을 볼 수 있다.
private SingletonService(){
  if (INSTANCE != null) {
    throw new AssertionError();
  }
}

 

둘 중 하나의 방식으로 만든 싱글톤 클래스를 직렬화 한다고 하면, Serializable 선언하는 것만으로는 부족하다.

직렬화란 자바 시스템 내부의 객체 또는 데이터를 외부 자바 시스템에서도 사용할 수 있도록 바이트 코드로 변환하는 기술을 의미하며 그 반대를 역직렬화라고 한다.

 

모든 인스턴스 필드를 일시적(transient)이라 하고, readResolve 메서드를 제공해야 한다. 그렇지 않으면 역직렬화에서 새로운 인스턴스가 만들어진다.

private Object readResolve() {
  // 실제 SingletonService를 반환하고, 가짜 SingletonService는 gc에 맡긴다.
  return instance;
}
  • 싱글톤임을 보장해주는 readResolve 메서드

 

Enum 방식

Enum은 상수들만 모아놓은 클래스로 생성자, 메서드를 가질 수 있다. 기본적으로 private 한 생성자를 가진다.

고정된 상수들의 집합으로 컴파일 시점에 모든 값을 알고 있어야하기 때문에 다른 패키지나 클래스에서 동적으로 값을 정해줄 수 없으며 상속이 불가능하다. (리플렉션도 방지)

public enum SingletonService {
    INSTANCE;

    public void logic () {
    System.out.println("싱글톤 객체 호출");
  }
}
  • public 필드 방식과 비슷하지만, 더 간결하고 추가 작업 없이 직렬화할 수 있다. 대부분의 상황에선 원소가 하나뿐인 열거 타입의 싱글톤을 만드는 것이 가장 좋은 방법이다.
  • 만들려는 싱글톤이 Enum 외의 클래스를 상속해야 한다면 해당 방법은 사용할 수 없다. 열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.

REFERENCES

https://jgrammer.tistory.com/entry/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-private-%EC%83%9D%EC%84%B1%EC%9E%90%EB%82%98-%EC%97%B4%EA%B1%B0-%ED%83%80%EC%9E%85%EC%9C%BC%EB%A1%9C-%EC%8B%B1%EA%B8%80%ED%84%B4%EC%9E%84%EC%9D%84-%EB%B3%B4%EC%A6%9D%ED%95%98%EB%9D%BC

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^