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

2025. 3. 27. 22:47·☕JAVA/이펙티브 자바


Singleton 패턴

 

싱글턴 패턴은 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 주로 무상태 객체나 설계상 유일해야 하는 시스템 컴포넌트에 사용되는 방식이다.

 

싱글턴 패턴을 사용할 때 주의할 점은 인터페이스로 정의하지 않은 싱글턴은 테스트에서 대체할 수 없다는 것이다. 싱글턴 클래스는 하나의 인스턴스를 생성하고 이를 재사용하는 방식으로 동작한다. 이로 인해 테스트 시 해당 객체를 가짜(Mock) 구현체나 다른 인스턴스로 교체할 수 있는 방법이 없어서 유연한 테스트가 어려워진다.

 

 

Singleton을 사용하는 방식

 

이러한 싱글턴을 만드는 방식은 크게 두가지가 있다. 아래에서 살펴보자.

 

필드방식

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

}



필드 방식에서 생성자는 필드에 public static final으로 선언한 Elvis.INSTANCE를 초기화 할 때 딱 한번 호출된다. private 이외에는 생성자가 없기 때문에 처음 초기화할때 인스턴스가 생성되어 전체 시스템에서 하나뿐임이 보장된다.

 

리플렉션 API를 사용하면 권한이 있는 클라이언트가 private 생성자를 호출할 수는 있지만 생성자를 수정하여 두번째 객체가 생성되려 할 때 예외를 던지는 방식으로 방어할 수 있다.

 

 

정적 팩터리 방식

public class Elvis {

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

 

정적 팩터리 방식에서는 Elvis.getInstance()가 항상 같은 객체의 참조를 반환하기 때문에 싱글턴이 보장이 된다.(리플렉션 에외는 동일하다.)

 

 

필드 방식 vs 정적 팩터리 방식

 

두 가지 방식을 비교해보면 public 방식은 해당 클래스가 싱글턴임이 API에 명확하게 드러난다는 점과 간결하다는 점에서 장점을 갖는다.

 

반면 정적 팩터리 방식은 API(외부 호출 방식)를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있고 제너릭 싱글턴 팩터리로 만들 수 있다.

 

예시

public class GenericBox<T> {
    private static GenericBox<?> EMPTY_BOX = new GenericBox<>();
    
    // 어떤 타입이든 사용할 수 있는 빈 박스를 만드는 메서드
    @SuppressWarnings("unchecked")
    public static <E> GenericBox<E> emptyBox() {
        return (GenericBox<E>) EMPTY_INSTANCE;
    }
}

// 사용 예시
GenericBox<String> stringBox = GenericBox.emptyBox(); // 문자열 박스
GenericBox<Integer> intBox = GenericBox.emptyBox();  // 정수 박스

 

 

 

또한 정적 팩터리 메서드 참조를 공급자로 사용할 수 있다는 점이 있다.

 

예시

import java.util.function.Supplier;

public class LazySingleton {
    private static final Supplier<LazySingleton> INSTANCE = 
        () -> SingletonHolder.INSTANCE;

    private LazySingleton() {}

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

    public static LazySingleton getInstance() {
        return INSTANCE.get();
    }
}

getInstance를 호출하기 전까지 객체가 생성되지 않는다. 또한 공급자를 변경하여 테스트에 용이하다는 장점이 있다.

 

아무튼 이러한 장점이 필요하지 않다면 public 필드 방식을 사용하는 것이 좋다고 한다...

 

 

Singleton 패턴의 직렬화 역직렬화 문제

 

직렬화는 객체의 상태를 바이트 스트림으로 저장한다. 그리고 역직렬화를 하면 JVM이 바이트 스트림을 기반으로 새로운 객체를 생성하게 된다. JVM은 여기서 직렬화된 데이터를 새로운 객체에 채우는데 이 과정에서 기존 싱글턴 인스턴스를 무시하고 새로운 인스턴스를 만들게 된다. 이말은 즉, 새로운 인스턴스가 만들어진다는 뜻인데 이를 방지하기 위해 싱글턴 클래스에 아래와 같은 readResolve 메서드를 추가하여 싱글턴을 보장할 수 있다.

private Object readResolve() {
	// 진짜 인스턴스를 반환하고, 가짜 인스턴스는 가비지 컬렉터에 맡긴다.
	return INSTANCE;
}

 

1. JVM이 역직렬화를 통해 새로운 객체를 생성.

2. 새로 생성된 객체의 readResolve() 메서드가 호출.

3. JVM이 readResolve라는 네이밍의 메서드를 찾고 readResolve()에서 기존 싱글턴 인스턴스를 반환하면 JVM은 새로 생성된 객체를 폐기하고 반환된 기존 객체를 사용.

 

 

열거 타입 방식

 

열거 타입 방식은 public 필드 타입과 비슷하지만 더 간결하고 역직렬화 시 별도의 설정 없이 JVM이 저장된 이름으로 기존의 인스턴스를 반환하기에 추가적인 노력 없이 직렬화 할 수 있다. 또한 리플렉션 공격도 막아준다고 한다.

 

대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이라고 한다.

다만 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다. (인터페이스 구현은 가능)

'☕JAVA > 이펙티브 자바' 카테고리의 다른 글

[이펙티브 자바] 아이템2 - 생성자에 매개변수가 많다면 빌더를 고려하라  (0) 2025.03.27
[이펙티브 자바] 아이템1 - 생성자 대신 정적 팩터리 메서드를 고려하라.  (0) 2024.07.20
'☕JAVA/이펙티브 자바' 카테고리의 다른 글
  • [이펙티브 자바] 아이템2 - 생성자에 매개변수가 많다면 빌더를 고려하라
  • [이펙티브 자바] 아이템1 - 생성자 대신 정적 팩터리 메서드를 고려하라.
승9
승9
  • 승9
    코드 창고
    승9
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • ☕JAVA (4)
        • 이펙티브 자바 (3)
      • 💡알고리즘 (0)
        • Greedy (0)
      • ❓고민 해결 (4)
        • http (2)
        • JAVA (0)
        • Spring Framework (1)
      • ♺인프라 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
승9
[이펙티브 자바] 아이템3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라
상단으로

티스토리툴바