백엔드/Java

자바 싱글톤(Singleton)을 구현하는 세가지 방법

dami97 2023. 11. 27. 16:30
반응형

싱글톤을 구현해야 하는 상황

  1. 하나만 유지하면 좋은 경우
  2. 반드시 하나만 유지해야 하는 경우 (여러개가 생성된다면 문제가 생기는 경우)

첫번째 방법

  • private 생성자 + public static final 필드를 사용하는 방법
public class Elvis {

	public static final Elvis INSTANCE = new Elvis();

	private Elvis() {}
}

장점

  • 간결하고 싱글턴임을 API에 드러낼 수 있다.

단점

  • 싱글톤을 사용하는 클라이언트 코드를 테스트하기 어려워진다.
  • 리플렉션으로 private 생성자를 호출할 수 있다.
// 리플렉션을 통해 싱글톤을 깨뜨리는 방법 예제
public static void main(String[] args) {
      try {
          Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();
          defaultConstructor.setAccessible(true);
          Elvis elvis1 = defaultConstructor.newInstance();
          Elvis elvis2 = defaultConstructor.newInstance();
          System.out.println(elvis1 == elvis2); // false
      
      } catch (...생략) {
          e.printStackTrace();
      }
}

// 이를 막기 위한 예제
public class Elvis implements IElvis, Serializable {

    public static final Elvis INSTANCE = new Elvis();
    private static boolean created; // 생성여부 필드 추가

    private Elvis() {
        if (created) {
            throw new UnsupportedOperationException("can't be created by constructor.");
        }

        created = true;
    }

		...생략

}
  • 역직렬화 할 때 새로운 인스턴스가 생길 수 있다.
// 역직렬화로 여러 인스턴스 만들기
public static void main(String[] args) {
  try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
      out.writeObject(Elvis.INSTANCE);
  } catch (IOException e) {
      e.printStackTrace();
  }

//역직렬화
  try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))) {
      Elvis elvis3 = (Elvis) in.readObject();
      System.out.println(elvis3 == Elvis.INSTANCE); // false
  } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
  }
}
// 역직렬화 시 새로운 인스턴스 생성 방지 코드 
public class Elvis implements IElvis, Serializable {

public static final Elvis INSTANCE = new Elvis();

		// 역직렬화 시 이 메서드가 오버라이드 처럼 활용 되지만 @Override를 표기 할 수 없음
    private Object readResolve() {
        return INSTANCE;
    }

		...생략
}

두번째 방법

  • private 생성자 + 정적 팩터리 메서드를 사용하는 방법
public class Elvis implements Singer {
		
    // public => private 선언
    private static final Elvis INSTANCE = new Elvis();
    // public => private 선언
    private Elvis() { }
    
    // 정적 팩터리 메서드
    public static Elvis getInstance() { return INSTANCE; }

	... 생략
}

장점

  • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
// 다시 싱글톤이 아니게 클라이언트 코드를 수정하지 않고 변경 가능
public static Elvis getInstance() { return new Elvis; }
  • 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. ( == 비교는 안됨..)
public class MetaElvis<T> {

    private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();

    private MetaElvis() { }

    // 제네릭 싱글톤 팩터리 메서드
    @SuppressWarnings("unchecked")
    public static <E> MetaElvis<E> getInstance() { return (MetaElvis<E>) INSTANCE; }

    //클라이언트 코드
    public static void main(String[] args) {
    	
        // 제네릭 타입이 다른 싱글톤 객체 생성 가능
        MetaElvis<String> elvis1 = MetaElvis.getInstance();
        MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
        
        
        System.out.println(elvis1.equals(elvis2));
        System.out.println(elvis1);
        System.out.println(elvis2);
        //출력 결과
        
        //true
        //me.whiteship.chapter01.item03.staticfactory.MetaElvis@133314b
        //me.whiteship.chapter01.item03.staticfactory.MetaElvis@133314b
    }

}
  • 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다.
package java.util.function;

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

// Singer 인터페이스를 구현하게 변경
public class Elvis implements Singer { ... 생략}
public class Concert {

    public void start(Supplier<Singer> singerSupplier) {
        Singer singer = singerSupplier.get();
        singer.sing();
    }

	//사용 코드
    public static void main(String[] args) {
        Concert concert = new Concert();
        concert.start(Elvis::getInstance); // Supplier<Singer> 메서드참조로 구현
    }
}

세번째 방법

  • 열거 타입
    • 가장 간결한 방법이며 직렬화와 리플렉션에도 안전하다.
    • 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
public enum Elvis{
	INSTANCE;

    // 메서드 정의
    public void leaveTheBuilding() {
    	System.out.println("안녕하세요");
    }
}

// 클라이언트 코드 사용 예제
public static void main(String[] args) {
    Elvis elvis = Elvis.INSTANCE;
    elvis.leaveTheBuilding();
}

 

 

출처

https://www.inflearn.com/course/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-1

 

 

반응형