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

2022. 5. 15. 16:47JAVA/Effective Java

싱글턴(Singleton)이란 무엇인가?

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 의미합니다. 싱글톤의 예시로는 함수와 같은 무상태(stateless) 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있습니다.

 

싱글턴 생성 첫번째 방식 : 필드 멤버에 public static final을 적용한 인스턴스를 클래스 초기화시 생성

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis(){}
		
	public void leaveTheBuilding()
	{
		System.out.println("call leaveTheBuilding");
	}
}

위와 같이 Elvis 타입의 INSTANCE 인스턴스는 클래스가 초기화할때 딱 한번만 인스턴스를 생성하여 저장합니다. public이나 protected 생성자가 없으므로 Elvis 클래스는 초기화가 될때 만들어진 인스턴스가 전체 시스템에서 하나뿐임을 보장합니다.

 

public 필드 방식의 장점

1.해당 클래스가 싱글턴임이 API에 명백히 드럼남

2. 필드에 직접적으로 접근하기 때문에 간결함

 

public 필드 방식의 단점

1. 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있음

 

다음 예제는 Private 클래스의 생성자 정보를 가져온 다음 setAccessible 메서드를 호출하여 true로 설정하고 Constructor.newInstance() 메서드를 호출하여 private 생성자를 호출합니다. 그리고 이때 제 2의 인스턴스가 생성되어 싱글톤의 신뢰성이 깨지게 됩니다.

public class PrivateInvoker {

	public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{

		Private p = Private.INSTANCE;
		
		Constructor<Private> con = (Constructor<Private>) p.getClass().getDeclaredConstructor();
		con.setAccessible(true);
		
		Private p2 = con.newInstance();	// private 생성자 호출하여, 제2의 인스턴스가 생성됨
		
		System.out.println(p.equals(p));	// Expected Output : true
		System.out.println(p.equals(p2));	// Expected Output : false
	}
}

class Private{
	public static final Private INSTANCE = new Private();
	
	private Private()
	{
		System.out.println("hello!");
	}
}

 

위와 같은 리플렉션 API을 활용한 private 생성자 호출을 막기 위해서는 두번째 객체가 생성되려 할때 예외를 던지게 하여 예방합니다.

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

 

2. 직렬화를 위해서 모든 인스턴스 필드를 transient라고 선언하고 readResolve 메서드를 제공해야 합니다.

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

 

싱글턴 생성 두번째 방식 : 정적 팩토리 메서드를 활용한 인스턴스 생성

public class Elvis{
	private static final Elvis INSTANCE = new Elvis();
	private Elvis()
	{
		
	}
	
	public static Elvis getInstance()
	{
		return INSTANCE;
	}
		
	public void leaveTheBuilding()
	{
		System.out.println("call leaveTheBuilding");
	}
}

 

위와 같이 Elvis.getInstance 메서드 호출시 항상 같은 객체의 참조를 반환하므로 제 2의 Elvis 인스턴스란 만들어지지 않습니다. 첫번째 방식과 달라진 점은 필드의 접근제어자가 private로 변경되었다는 점입니다. 그리고 공통점은 두 방식 모두 생성자의 접근제어자가 private라는 점입니다.

 

정적 팩토리 방식의 장점

  • 마음이 바뀌면 API를 변경하지 않고도 싱글턴이 아니게 변경할 수 있습니다.
  • 정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있습니다.
  • 정적 팩토리의 메서드 참조를 공급자(supplier)로 사용할 수 있습니다.
		Supplier<Elvis> sup = new Supplier<Elvis>() {
			
			@Override
			public Elvis get() {
				return Elvis.getInstance();
			}
		};
		
		Elvis e = sup.get();

정적 팩토리 방식의 단점

  • public 필드 방식과 마찬가지로 권한이 있는 클라이언트가 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있습니다.
  • 직렬화를 위해서 모든 인스턴스 필드를 transient라고 선언하고 readResolve 메서드를 제공해야 합니다. 이와 같이 하지 않으면 역직렬화 할때마다 새로운 인스턴스가 생성됩니다.

 

싱글턴 생성 세번째 방식 : 원소가 하나인 열거(enum) 타입을 선언

public enum Elvis{
	INSTANCE;
	
	public void leaveTheBuilding()
	{
		System.out.println("call leaveTheBuilding");
	}
}

열거 타입 방식의 장점

  • 코드가 간결함
  • 추가적인 코드없이 직렬화 할 수 있음
  • 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 예방함

열거 타입 방식의 단점

  • 만들려는 싱글턴 클래스가 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없습니다.

 

리플렉션 API(Reflection API)이란 무엇인가?

구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해주는 자바 API입니다.

https://yonghwankim-dev.tistory.com/376

 

[Java] Java Reflection API

1. Reflection API란 무엇인가? Reflection API는 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 변수 등등)에 접근할 수 있게 해주는 자바 API입니다. 2. Reflection API 사례 public clas..

yonghwankim-dev.tistory.com

 

직렬화/역직렬화란 무엇인가?

자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터를 변환하는 기술입니다. 반대로 역직렬화는 바이트로 변환된 데이터를 다시 객체로 변환하는 기술입니다.

https://yonghwankim-dev.tistory.com/378

 

[Java] 직렬화(Serialization)

1. 직렬화(Serialization)란 무엇인가? 직렬화란 자바의 객체의 상태에서 바이트 스트림으로 변환하는 기술을 말합니다. 비직렬화는 반대로 바이트 스트림에서 다시 자바의 객체로 변환하는 기술입

yonghwankim-dev.tistory.com

 

References

source code : https://github.com/yonghwankim-dev/effective_java
자바 직렬화: readResolve와 writeReplace
[Design Pattern] 싱글턴 패턴(Singleton pattern)
Reflection API 간단히 알아보자
[Java] 직렬화(Serialization)란 무엇일까?