이 글은 몇몇 크루들과 이펙티브 자바 스터디를 하며 정리한 내용입니다. 🙌

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

하나의 인스턴스만 생성할 수 있는 것이 싱글턴(Singleton) 패턴이다. 참고로 싱글턴을 사용할 경우 이것을 사용하는 클라이언트를 테스트하기가 어렵다. 싱글턴 객체의 생성지점을 제어하기 어려우므로 mock으로 대체하기가 어렵기 때문이다. 싱글턴 객체를 구현하기 위해서는 다음 3가지 방법을 사용할 수 있다.


1) private 생성자 & public static final 필드

public class Earth {
	public static final Earth INSTANCE = new Earth();

	private Earth() {
		...
	}
}

생성자의 접근제어자가 private이므로 인스턴스는 오직 INSTANCE 필드를 초기화할 때 단 한번만 생성된다. 일반적인 경우 클라이언트는 이 부분에 대한 권한이 전혀 없지만 예외적으로 AccessibleObject.setAccessible을 사용한다면 private 생성자를 호출할 수 있기는 하다. (방어를 위해서는 두번째 생성자 호출시 예외발생)

  • 장점
    • public static final 필드를 사용할 경우 싱글턴이라는 것이 분명히 드러난다. (final이므로 재정의 할 수 없다.)
    • 간결하다.

2) private 생성자 & 정적 팩터리 메서드

public class Earth {
	private static final Earth INSTANCE = new Earth()'

	pirvate Earth() {
		...
	}

	public static Earth getInstance() {
		return INSTANCE;
	}
}
  • 장점
    • 싱글턴이 아닌 경우로 리팩토링 할 경우 변경에 유연하다.
    • 정적 팩터리를 제네링 싱글턴 팩터리로 만들 수 있다.
    • 정적 팩터리 메소드를 Supplier로 사용할 수 있다. (일급 함수로 사용 가능)

위 방법에서 직렬화 시 주의할 점

직렬화는 객체를 바이트로 변환해 데이터를 외부 시스템에 영구적으로 저장하거나 사용할 수 있도록 하는 것을 말한다. 직렬화 후 다시 역직렬화 할 때(readObject() 사용시) 싱글톤임에도 불구하고 새로운 인스턴스가 생성된다.

이를 방지하기 위해서는 직렬화 하는 객체의 필드를 transient 선언(직렬화 대상에서 제외하고 readResolve() 메서드를 정의하여 기존에 생성된 객체를 반환하도록 해야 한다. 이때도 readObject() 호출 시 새로운 인스턴스가 생성되기는 하지만 해당 인스턴스를 가짜 인스턴스로 간주하고 무시하여 GC가 처리한다.

private Object readResolve() {
	return INSTANCE;
}

3) Enum 타입으로 선언

public enum Earth {
	INSTANCE;

	...// 관련 메서드 정의
}

열거타입으로 선언할 경우 매우 간결하고 특별한 노력 없이 직렬화 관련 문제도 해결된다. 또한 리플렉션 시에도 싱글톤임을 보장해준다. 하지만 열거 타입의 본래 사용의도와 어긋나므로 어색해보일 수 있다.



🌩 [아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라

객체지향적으로 보았을 때 안티패턴이기는 하지만 필요시 정적 필드와 정적 메서드만을 모아둔 utility 성향의 클래스를 생성하게 될 때가 있다.

  • 자바의 경우 java.util.Arrays, java.util.Collections와 같이 배열과 관련된 메서드를 모아 놓거나 특정 인터페이스 구현체를 생성해주는 팩터리 역할을 하는 경우 필요하다.

이 경우 클래스의 인스턴스화를 막아야 하는데 이때는 기본 생성자를 private으로 선언하여 명시해주어야 한다.

  • 컴파일러는 기본 생성자가 명시되어 있지 않으면 자동으로 기본 생성자를 만들어서 인스턴스화가 가능하도록 한다.
  • 실수로 클래스 내에서 private 생성자를 사용하지 않도록 주의해야 한다.
  • 기본 생성자가 막혀있다면 하위 클래스에서 상위 클래스의 생성자에 접근할 수 없으므로 상속을 불가능하게 하는 효과도 있다.

사용하지 않을 생성자를 코드에 명시하는 것이므로 직관적인 코드는 아니라는 단점도 있다.