[Effective Java] item3 - private 생성자나 열거 타입으로 싱글톤임을 보증하라

신민철·2023년 4월 8일
3

Effective Java

목록 보기
3/23
post-thumbnail

싱글톤 혹은 싱글턴(Singleton), (저는 싱글톤이라고 하겠습니다.)이란, 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

싱글톤을 만드는 방식은 크게 두가지이다.

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() { ... }
	
	public void leaveTheBuilding() { ... }
}

위에서 private 생성자는 public static final 필드인 Elvis.INSTANCE를 한 번 초기화할 때만 호출된다.

public이나 protected 생성자가 없으므로 객체가 딱 하나라는 것이 보장된다.

예외적인 상황이 있는데, AccessibleObject.setAccessable을 사용하면 private 생성자를 호출할 수 있다.

→ 생성자 내에 두 번째 객체가 생성되려고 할 때 예외를 던지면 해결 가능

생성자를 했으니 우리가 item1에서 배운 정적 팩토리 메소드를 활용한 방식도 있을 것이다.

public class Elvis {
	private static final Elvis INSTANCE = new Elvis();
	private Elvis() { ... }
	public static Elvis getInstance() { return INSTANCE; }

	public void leaveTheBuilding() { ... }
}

Elvis.getInstance에서는 미리 생성된 private final 필드를 항상 반환해주므로 인스턴스가 하나로 유지된다는 것을 보장한다. 다만 위의 리플렉션의 예시처럼 똑같은 예외는 적용된다.

각각의 방식에는 장점과 단점이 있다.

첫번째 방식은 API를 보기만 해도 싱글톤임을 확인할 수 있고 코드가 간결하다는 장점이 있다.

정적 팩토리 메소드 방식은 싱글톤이 아니게 수정해야 할 필요가 있을 때 큰 문제 없이 수정이 가능하다는 장점이 있고 제네릭 싱글턴 팩토리로 만들 수 있고, 정적 팩토리의 메소드 참조를 공급자로 사용할 수 있다는 것이 장점이다.

“정적 팩토리를 제네릭 싱글톤 팩토리로 만들 수 있다.”

이 말을 다시 한번 살펴보자.

싱글톤인데 제네릭이 적용되어 요청 타입 변수에 맞게 객체의 타입을 바꿔준다. reverseOrder()를 한번 살펴보자.

public static <T> Comparator<T> reverseOrder() {
        return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}

private static class ReverseComparator
    implements Comparator<Comparable<Object>>, Serializable {

    private static final long serialVersionUID = 7207038068494060240L;

    static final ReverseComparator REVERSE_ORDER
        = new ReverseComparator();

    public int compare(Comparable<Object> c1, Comparable<Object> c2) {
        return c2.compareTo(c1);
    }

    private Object readResolve() { return Collections.reverseOrder(); }

    @Override
    public Comparator<Comparable<Object>> reversed() {
        return Comparator.naturalOrder();
    }
}

ReverseComparator에는 싱글톤 객체인 ReverseComparator가 있고, 이것을 reverseOrder()가 반환해주는 역할을 한다. ReverseComparator는 Comparable한 Object는 모두 허용하기 때문에 제네릭의 장점을 살릴 수 있는 것이다.

다음은 “정적 팩토리의 메소드 참조를 공급자로 사용할 수 있다” 이다.

이것은 Item 43, 44에서 나와있으니 나중에 정리하도록 하자.

근데 여기서 직렬화(Serialization)라는 개념이 나온다.

📖 여기서 잠깐 짚고 넘어가는 '직렬화(Serialization)'

우리가 만든 클래스가 파일에 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면 반드시 Serializable 인터페이스를 구현해야 한다. 직렬화는 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 변환을 시켜주고 바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)을 아울러서 이야기한다.

public interface Serializable {
	/* 여기에는 아무 내용도 없다 - 직접 구현 */
}

여기에서 다시 위의 내용으로 돌아가보자. 직렬화를 시킬 일이 있다면 어떻게 될까?

역직렬화를 하는 과정에서 받아오는 객체는 싱글톤으로 정의한 그 객체가 아니라, readObjecy를 통해 변경된 새로운 객체(해시코드 값은 같겠지만 같은 객체는 아닌 것이다.)이기 때문에, 싱글톤 법칙이 깨지는 것이다.

이를 해결하기 위해서는

private Object readResolve() { return INSTANCE; }

이렇게 생성하여 해당 싱글톤 객체를 반환하도록 보장해줘야 하는 것이다.

마지막으로 싱글톤을 만드는 방식 중에 열거 타입을 선언하는 방식이 있다.

public enum Elvis {
	INSTANCE;

	public void leaveTheBuilding() { ... }
}

public 필드 방식과 비슷하지만 훨씬 더 간결하고 추가 노력 없이 직렬화할 수 있다. 그리고 리플렉션 상황에서도 인스턴스가 더 생성되지 않는다는 장점이 있다.

대부분의 상황에서는 좋은 방법이지만, Enum 이외의 다른 클래스를 상속받는 경우에는 이 방법은 사용이 불가능하다.

핵심 정리
싱글톤을 만드는 방식에는 3가지가 있는데, public 생성자 방식, 팩토리 메소드 방식, enum 방식이 있다. 각자는 reflection에 대한 예외처리를 해주어야 하고 직렬화를 위해선 가끔 readResolve() 같이 구현을 해야 할 때도 있다.

0개의 댓글