ITEM3. private 생성자나 열거 타입(enum)으로 싱글턴임을 보장하라
싱글턴(Singleton) 패턴이란? 싱글턴은 클래스의 인스턴스를 오직 하나만 생성하고, 해당 인스턴스에 전역적으로 접근할 수 있도록 보장하는 디자인 패턴이다. 특정 클래스의 객체가 유일해야 하는 상황에서 사용된다.
싱글턴 구현 방식
1. private 생성자와 public static 필드
이 방식은 싱글턴 객체를 정적 필드로 미리 생성해두고, 이 객체를 반환하는 방식이다. 생성자를 private으로 숨겨 외부에서 새로운 객체를 생성할 수 없도록 제한한다.
class Singleton {
// 유일한 인스턴스를 static final로 선언
public static final Singleton INSTANCE = new Singleton();
// private 생성자: 외부에서 객체 생성 불가
private Singleton() {}
public void showMessage() {
System.out.println("Hello, Singleton!");
}
}
public class Main {
public static void main(String[] args) {
// 유일한 인스턴스 사용
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2); // true
singleton1.showMessage(); // Hello, Singleton!
}
}
- 초기화 시점: 클래스 로드 시점에 인스턴스를 생성한다.
- 간결함: 코드가 간단하고 직관적이다.
- 단점: 클래스가 로드될 때 인스턴스를 생성하므로, 객체 생성 비용이 크거나 애플리케이션에서 사용되지 않을 경우 비효율적일 수 있다.
2. private 생성자와 정적 팩터리 메서드
이 방식에서는 객체 생성을 private 생성자로 제한하고, 정적 팩터리 메서드를 통해 유일한 인스턴스를 반환한다.
class Singleton {
// 유일한 인스턴스
private static final Singleton INSTANCE = new Singleton();
// private 생성자: 외부에서 객체 생성 불가
private Singleton() {}
// 정적 팩터리 메서드
public static Singleton getInstance() {
return INSTANCE;
}
public void showMessage() {
System.out.println("Hello, Singleton!");
}
}
public class Main {
public static void main(String[] args) {
// 정적 팩터리 메서드를 통해 인스턴스 사용
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true
singleton1.showMessage(); // Hello, Singleton!
}
}
- 지연 초기화 가능: 필요에 따라 객체를 생성하는 방식으로 변경 가능하다.
- 변경 용이: 정적 팩터리 메서드 내부에서 생성 방식(예: 객체 풀, 캐싱)을 유연하게 변경할 수 있다.
3. 싱글턴을 열거 타입(enum)으로 구현
열거 타입(enum)을 사용하면 가장 간결하고 안전하게 싱글턴을 구현할 수 있다. 자바의 열거 타입은 본질적으로 싱글턴이므로, 직렬화와 리플렉션에 의한 공격에도 안전하다.
public enum Singleton {
INSTANCE;
public void showMessage() {
System.out.println("Hello, Singleton with Enum!");
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2); // true
singleton1.showMessage(); // Hello, Singleton with Enum!
}
}
- 안전성: 자바의 enum은 JVM 차원에서 싱글턴을 보장하며, 리플렉션이나 직렬화로부터 안전하다.
- 간결함: 코드가 매우 간단하고 가독성이 높다.
싱글턴 패턴 사용 시 주의사항
멀티스레드 환경에서 안전성 보장:
위의 모든 구현 방식은 자바에서 스레드 안전성을 기본적으로 보장한다. 특히, 정적 팩터리 메서드를 사용한 방식에서는 **lazy initialization (지연 초기화)**를 구현할 경우 추가적인 동기화 처리가 필요할 수 있다.
직렬화와 리플렉션 공격 방지:
기본적인 private 생성자 방식은 직렬화와 리플렉션 공격에 취약할 수 있으므로, readResolve 메서드를 통해 이를 방지하거나 enum 방식을 사용하는 것이 안전하다.
// readResolve를 사용한 직렬화 안전 구현
private Object readResolve() {
return INSTANCE; // 새로운 객체가 생성되지 않도록 함
}
싱글턴 패턴의 활용 예시
Runtime 클래스:
자바의 java.lang.Runtime 클래스는 싱글턴으로 구현되어 있다. 애플리케이션 전체에서 하나의 Runtime 인스턴스만 존재한다.
Runtime runtime1 = Runtime.getRuntime();
Runtime runtime2 = Runtime.getRuntime();
System.out.println(runtime1 == runtime2); // true
Logging 클래스:
로그를 기록하는 클래스는 애플리케이션 전역에서 하나의 인스턴스만 필요하므로, 싱글턴으로 구현되는 경우가 많다.
Thread Pool, Database Connection Pool:
리소스를 관리하는 객체들은 싱글턴으로 구현되어야 관리가 용이하다.