728x90
3. private 생성자나 열거 타입으로 싱글턴임을 보장하라.
싱글턴
- 인스턴스를 오직 하나만 생성할 수 있는 클래스
- ex ) 함수와 같은 무상태 객체, 설계상 유일해야하는 시스템 컴포넌트
- 클래스를 싱글턴으로 만들면 클라이언트를 테스트하기 어렵다.
- 싱글턴 인스턴스는
private
생성자를 가지므로 새로 생성하여 테스트할 수 없기 때문이다. - 실제 싱글턴 인스턴스에 접근하여 테스트할 경우 전역상태이므로 테스트 독립성을 깨뜨린다.
- 싱글턴 인스턴스는
- 인터페이스를 구현해 만든 싱글턴일 경우 테스트가 가능하다.
현재는 static에 대해 mocking이 가능하지만 권장하지 않는다. 인터페이스를 통해 의존성을 주입하자.
인터페이스를 구현한 싱글턴
// 인터페이스 정의
public interface DatabaseConnection {
void connect();
}
// 싱글턴 구현
public class RealDatabaseConnection implements DatabaseConnection {
private static final RealDatabaseConnection INSTANCE = new RealDatabaseConnection();
private RealDatabaseConnection() {}
public static RealDatabaseConnection getInstance() {
return INSTANCE;
}
public void connect() {
}
}
// 테스트
class RealDatabaseConnectionTest {
@Test
void testConnect() {
// 가짜 DatabaseConnection 구현체 생성
FakeDatabaseConnection fakeDatabaseConnection = new FakeDatabaseConnection();
// connect() 메서드 호출
fakeDatabaseConnection.connect();
// 연결이 성공했는지 확인
assertTrue(fakeDatabaseConnection.isConnected());
}
}
// 가짜 싱글턴 객체
class FakeDatabaseConnection implements DatabaseConnection {
private boolean connected = false;
@Override
public void connect() {
this.connected = true;
}
public boolean isConnected() {
return this.connected;
}
}
싱글턴을 만드는 방식
1. public static final 필드
- 싱글턴을 public static 멤버로 직접 접근한다.
- 싱글턴 인스턴스를
public
으로 공개
- 싱글턴 인스턴스를
- 생성자는
private
으로 감춰놓는다.
// 코드 3-1 public static final 필드 방식의 싱글턴 (23쪽)
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("I'm outta here!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다.
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE; // 직접 접근
elvis.leaveTheBuilding();
}
}
- 해당 클래스가 싱글턴임이 api에 명확하게 드러난다.
- 간결하다.
리플렉션
- private 생성자라도 리플렉션 API인 AccessibleObject.setAccessible() 을 사용해 private 생성자를 호출할 수 있다.
- 이를 방어하기 위해 생성자가 두번 이상 호출될 경우 에러를 반환하게 할 수 있다.
public class SingletonBreaker {
public static void main(String[] args) throws Exception {
Singleton singleton1 = Singleton.getInstance();
// 리플렉션을 사용하여 private 생성자에 접근
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton1 == singleton2); // false 출력
}
}
2. 정적 팩터리 방식의 싱글턴
// 코드 3-2 정적 팩터리 방식의 싱글턴 (24쪽)
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {
System.out.println("I'm outta here!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance(); // 정적 팩토리로 접근
elvis.leaveTheBuilding();
}
}
- 싱글턴 인스턴스를
private
으로 하고 정적 팩터리 (getInstance()
)로 접근한다. - 정적 팩터리를 사용하면 클라이언트의 코드를 수정하지 않고 싱글턴이 아니도록 변경할 수 있다.
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
public class GenericSingletonFactory {
private static final Set EMPTY_SET = new HashSet();
// 제네릭 싱글턴 팩터리 메서드
@SuppressWarnings("unchecked")
public static <T> Set<T> emptySet() {
return (Set<T>) EMPTY_SET;
}
}
// 사용 예
Set<String> stringSet = GenericSingletonFactory.emptySet();
Set<Integer> integerSet = GenericSingletonFactory.emptySet();
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
public class SupplierFactory {
public static Integer createInteger() {
return Integer.valueOf(42);
}
}
Supplier<Integer> integerSupplier = SupplierFactory::createInteger;
Integer value = integerSupplier.get(); // 42
직렬화, 역직렬화
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private transient String data;
private Singleton() {
data = "Singleton Data";
}
public static Singleton getInstance() {
return INSTANCE;
}
// 역직렬화 시 호출
private Object readResolve() {
// 싱글턴의 기존 인스턴스를 반환
return INSTANCE;
}
public String getData() {
return data;
}
}
- 직렬화된 객체를 읽어올 때(역직렬화) Java는 새로운 객체를 생성한다.
- 싱글턴 특성을 유지하려면 단순히
Serializable
을 구현하는 것으로는 부족하다.- 직렬화에서 제외할 필드를 표시(transient)하고, 역직렬화 시 기존 싱글턴 인스턴스를 반환하도록 구현해야 한다.
3. Enum 타입의 싱글턴 🌟
// 열거 타입 방식의 싱글턴 - 바람직한 방법 (25쪽)
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("지금 나갈께!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
- 원소가 하나인 enum 타입을 선언한다.
- 싱글턴을 만드는 가장 좋은 방법이다.
- 간결하고 추가 노력 없이 직렬화가 가능하다.
- 리플렉션 공격을 막아준다.
enum
은 상수 집합이며 각 필드는public static final
필드이다.- 생성자는
private
이다. - 기본적으로
Serializable
을 구현한다. - 직렬화, 역직렬화시 같은 인스턴스를 반환한다.
- 리플렉션을 사용해도 인스턴스 생성이 불가능하다.
jvm
에서 생성자 호출을 막는다.
- 생성자는
728x90
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 5. 자원을 직접 명시하지 말고 의존 객체 주입(DI)을 사용하라. (0) | 2025.01.05 |
---|---|
[Effective Java] Item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2025.01.05 |
[Effective Java] Item 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2025.01.05 |
[Effective Java] Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2025.01.05 |
[Effective Java] 2장. 객체 생성과 파괴 - 0. 들어가기 (0) | 2025.01.05 |