규칙 3. private 생성자나 enum 타입으로 싱글턴임을 보증하라

[싱글턴](dp-singleton)은 객체를 하나만 만들 수 있는 클래스다.

singleton 구현 방법

public static final 상수(before JDK 1.5)
public class Single {
	public static final Single INSTANCE = new Single();

	private Single() { ... }
}

문제점

  • 리플렉션으로 private 생성자 호출 가능

  • 생성자에서 에러날 경우 예외처리 불가능 → static 초기화 블럭으로 해결 가능

static factory 메서드(before JDK 1.5)
public class Single {
	private static final Single INSTANCE = new Single();
	private Single() { ... }
	public static Single getInstance() {
		return INSTANCE;
	}
}

문제점

  • 리플렉션으로 private 생성자 호출 가능

  • 위 두 방법에서 [직렬화](#serializable)가능 클래스로 만드려면 클래스 선언에 `implements Serializable`을 추가하는 것으로는 부족하다.

  • 클래스 선언에 implements Serializable 추가

  • 모든 객체 필드에 transient 선언

  • [readResolve() 추가](#item77)

    		```java
    		private Object readResolve() {
    			// 동일한 객체가 반환되도록 하는 동시에,
    			// 가짜 객체는 gc가 처리하도록 만든다.
    			return INSTANCE;
    		}
    		```
    - thread safe하려면 synchronized 적용
    ```java
    public static synchronized Single getInstance() { ... }
    ```
Initialization on demand holder idiom
  • jvm 의 class loader의 매커니즘과 class의 load 시점을 이용하여 내부 class를 생성시킴으로 thread 간의 동기화 문제를 해결

  • lazy initialization

public class Singleton {
	private Singleton() { }

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

	public static Singleton getInstance() {
	        return SingletonHolder.INSTANCE;
	}
}
*enum*을 이용하는 방법(after JDK 1.5)
public enum Single {
	INSTANCE;
	...
}
  • 직렬화 자동으로 처리된다.

  • 리플렉션 공격에도 안전하다.

  • Enum 생성은 Thread-safe하지만, 내부 메서드들은 Thread-safe를 보장하지 않는다.

why?

  • 선언된 상수 이외의 다른 객체는 존재할 수 없다는 확실한 보장이 생긴다(JVM이 해주는 보장).

  • enum 타입은 Comparable 인터페이스, Serializable 인터페이스가 구현되어 있다.

참고

serializable

객체의 내용을 파일에 저장하거나 네트워크로 전송하기 위해서 스트림으로 만드는 작업(바이트 단위로 변환)

  • Serializable 인터페이스 구현

  • 모든 필드 또한 Serializable 인터페이스 구현

  • 제외하고자하는 필드는 transient

example

public class Test {
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		SerializerTest test = new SerializerTest();
		test.serialization();
		test.deserialization();
	}
}

class SerializerTest {
	private String filePath = "/Users/yeongjun/Desktop/test.ser";
	private User user;

	public void serialization() throws IOException {
		user = new User("yj", 26, "pwd");
		FileOutputStream f = new FileOutputStream(filePath);
		ObjectOutputStream o = new ObjectOutputStream(f); // 직렬화 클래스
		o.writeObject(user); // 파라미터로 넘긴 객체를 스트림으로 만들어서 출력하는 메서드
		o.close();
	}

	public void deserialization() throws IOException, ClassNotFoundException {
		FileInputStream f = new FileInputStream(filePath);
		ObjectInputStream o = new ObjectInputStream(f); // 역직렬화 클래스
		user = (User)o.readObject(); // 입력된 스트림으로부터 객체를 만들어서 반환하는 메서드
		o.close();
		System.out.println(user.toString());
	}
}

class User implements Serializable {
	private static final long serialVersionUID = 1L; // 이건 왜?
	private String name;
	private int age;
	private transient String password;

	public User(String name, int age, String password) {
		this.name = name;
		this.age = age;
		this.password = password;
	}

	@Override
	public String toString() {
		return "User{name='" + name + '\'' + ", age=" + age + ", password='" + password + "\'}";
	}
}