싱글턴(Singleton) 패턴은 클래스의 인스턴스를 오직 하나만 생성하여 공유하는 방식입니다. 객체를 호출할 때마다 새로운 인스턴스를 만들지 않고, 동일한 인스턴스를 반환하는 방식이므로 메모리 낭비를 방지하고, 동작의 일관성을 유지할 수 있습니다.

싱글턴을 구현하는 주요 방식은 다음과 같습니다.

📌 public static final 필드를 이용한 방식

1
2
3
4
5
6
7
8
9
public class Elvis {

    public static final Elvis INSTANCE = new Elvis(); // 유일한 인스턴스

    private void Elvis() { ... } // private 생성자

    private void leaveTheBuilding() { ... }

}

장점

  1. 싱글턴임이 명확: 클래스 API에서 싱글턴임이 명확하게 드러납니다. Elvis.INSTANCE로 해당 클래스가 싱글턴임을 쉽게 파악할 수 있습니다.
  2. 간결함: 코드가 매우 간결합니다. 별도의 메서드 없이 인스턴스를 호출할 수 있어 간편합니다.

단점

  • 리플렉션을 통한 침해 가능성: 리플렉션을 사용하면 private 생성자를 우회할 수 있어, 새로운 인스턴스를 생성할 수 있습니다. 이를 막기 위해서는 생성자에서 예외를 던지도록 수정해야 합니다.
1
2
3
4
5
private Elvis() {
    if (INSTANCE != null) {
        throw new IllegalStateException("Instance already exists");
    }
}


📌 정적 팩토리 메서드를 사용하는 방식

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Elvis {

    private static final Elvis INSTANCE = new Elvis(); // 유일한 인스턴스

    private void Elvis() { ... } // private 생성자

    public static Elvis getInstance() { return INSTANCE; } // 정적 팩토리 메서드

    private void leaveTheBuilding() { ... }

}

장점

  1. 유연성: API를 바꾸지 않고 싱글턴에서 벗어날 수 있습니다. getInstance() 메서드의 반환 타입을 조정하면, 다중 인스턴스를 지원하는 클래스로 변경할 수 있습니다.
  2. 서브클래싱과 인터페이스: 정적 팩토리 메서드는 Supplier 인터페이스에 대응할 수 있어, 더 유연하게 활용할 수 있습니다.
  3. 다중 인스턴스 지원: 정적 팩토리 메서드를 통해 특정 조건에 따라 스레드별로 다른 인스턴스를 반환할 수도 있습니다.

단점

  • 여전히 리플렉션 문제가 존재합니다.



직렬화 문제 해결

앞에서 설명한 두 방식(정적 필드와 정적 팩토리 메서드)은 직렬화할 때마다 새로운 인스턴스가 생성될 수 있습니다. 이를 막기 위해서는 readResolve() 메서드를 구현하여, 역직렬화(Deserialization) 과정에서 동일한 인스턴스를 반환하도록 해야 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import java.io.Serializable;

public class Elvis implements Serializable {

    ...

    private Object readResolve() {
        returm INSTANCE;
    }
}


📌 열거 타입의 방식의 싱글턴

1
2
3
4
5
public enum Elvis {
    INSTANCE; // 유일한 인스턴스

    public void leaveTheBuilding() { ... }
}

장점

  1. 리플렉션과 직렬화 공격에 안전: 열거 타입은 리플렉션을 통한 침해직렬화에 의한 새로운 인스턴스 생성을 방지합니다.
  2. 간결함: 코드가 매우 간단하고 안전하며, 자바에서 기본적으로 제공하는 기능을 활용하기 때문에 확실한 싱글턴 보장이 가능합니다.

대부분의 경우 열거 타입을 사용한 싱글턴이 가장 적합한 방법입니다. 자바의 Enum직렬화와 리플렉션 공격에 대한 보호 기능을 기본적으로 제공하며, 인스턴스의 유일성을 강력하게 보장합니다.

단점

  • 다른 클래스를 상속할 수 없음: 열거 타입은 상속이 불가능하므로, 싱글턴 클래스가 다른 클래스를 상속해야 하는 경우에는 사용할 수 없습니다. (열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있습니다)