백엔드 개발자 블로그

[Effective Java] Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 본문

Java

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

backend-dev 2024. 3. 20. 22:55

싱글턴은 하나의 프로그램 내에 하나의 인스턴스만을 가질 수 있도록 하는 전략 패턴입니다.

 

싱글턴을 사용하는 대표적인 예로는 stateless 객체나 설계적으로 프로그램 내에 하나만 존재해야하는 시스템 컴포넌트 들이 있습니다. 하지만 싱글턴의 경우 interface를 통해 구현하여 만든 것이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문에 클라이언트를 테스트하기 어려워질 수 있다는 단점이 존재합니다.


싱글턴 패턴을 만드는 방식

1. public static final 필드 방식의 싱글턴

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

1. 생성자를 private 접근 제어자로 설정합니다.

2. public static으로 설정한 INSTANCE멤버 변수를 생성하여 유일한 인스턴스에 접근할 수 있는 수단을 만듭니다.

  • private으로 설정된 생성자는 INSTANCE를 처음 초기화할 때만 한번 호출하며 프로그램 내에 인스턴스가 하나만 존재하도록 합니다.

장점 :  해당 클래스가 싱글턴임을 API에서 명백하게 드러낼 수 있고 간결하다는 점이 있습니다.


2. 정적 팩터리 방식의 싱글턴

1. 생성자를 private 접근 제어자로 설정합니다.

2. 유일한 인스턴스에 접근할 수 있는 수단으로 private static으로 설정한 INSTANCE멤버 변수를 생성해둬야합니다.

  • 1번 방식과 다른점이라면 INSTANCE 멤버 변수의 접근제어자가 public이 아닌 private이라는 점입니다.

3. 정적 팩터리 메서드로 INSTANCE에 접근을 할 수 있으며 만들어진 인스턴스를 가져오는 식으로 운영되기에 새로운 인스턴스가 생성될 일이 없습니다.

 

장점

  • API를 바꾸지 않고도 싱글턴이 아니게 변경을 할 수 있습니다. (제너릭 싱글턴 팩터리)
  • 정적 팩터리 메서드를 공급자(Supplier)로 활용할 수 있습니다

 

예시 코드는 다음과 같습니다.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static final Singleton getInstance() { return INSTANCE; } // 정적 펙터리 메서드
    ...
}

 

 


1, 2번 방식의 주의할 점

  • 리플렉션 API인 AccessibleObject.setAccessible을 통해 private 생성자를 호출하게 된다면 새로운 작업이 필요합니다.
    • 두개 이상의 인스턴스가 생성될 수 있으므로 생성자 내에서 두번째 객체를 생성하려고 할 때 exception을 발생시키도록 설정해줘야 합니다.
  • 직렬화를 하려면 단순히 Serializable을 구현한다고 선언하는 것 뿐만 아니랏 새로운 작업이 필요합니다.
    • 모든 인스턴스 필드를 transient( Serialize하는 과정에 제외하고 싶은 경우 선언하는 키워드 )이라고 선언해줘야 합니다.
    • readResolve메서드를 통해 인스턴스를 반환해야합니다. (그렇지 않을 경우 새로운 인스턴스가 생성됩니다.)
    • 예시코드는 아래와 같습니다.
public class Singleton implements Serializable {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    private Singleton readResolve() {
        // 진짜 Singleton 인스턴스를 반환 후 가짜 인스턴스는 가비지 컬렉터로~
        return INSTANCE;
    }
}

3. 열거타입(Enum)타입의 싱글턴

public enum Singleton {
    INSTANCE;
    public void method() {}
    ...
}
  • 1번 public 필드 방식과 유사하지만 더 간결합니다.
  • 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 추가적인 노력 없이 다른 인스턴스가 생성되는 것을 완벽하게 막아줍니다.
  • 열거타입의 싱글턴은 원소가 하나뿐이라 부자연스러워 보일 수 있지만 대부분의 상황에서는 가장 좋은 방법입니다. 단, 만들고자하는 싱글턴이 Enum이외의 클래스를 상속해야한다면 해당 방법은 사용할 수 없습니다.