Java

[Effective Java] 2-3. 객체 생성과 파괴: private 생성자나 열거 타입으로 싱글턴임을 보장하라

noahkim_ 2024. 12. 27. 03:05

조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.

 

1. 싱글턴

  • 특정 클래스의 인스턴스를 오직 하나만 생성하는 디자인 패턴
항목 내용
주요 목적 - 전역적으로 공유되는 인스턴스 제공
- 인스턴스 생성 비용 절감
주로 사용되는 곳 - 시스템 설정 관리자
- 로깅 객체
- 설계상 유일해야 하는 객체 (DB 커넥션 풀 등)
특징
- 일반적으로 무상태(stateless) 객체로 설계
- 함수적 역할 수행이 많음 (입력 → 출력만 있음)
- 전역 접근 지점 제공 (Class.getInstance())

 

2. 생성 방식

예) 인스턴스 필드

더보기
public class Yundle {
    public static final Yundle INSTANCE = new Yundle();
    private Yundle() {}

    public static void main(String[] args) {
        Yundle yundle = Yundle.INSTANCE;
    }
}
  • static final 필드값이 초기화됨

 

예) 정적 팩토리

더보기
public class MetaElvis<T> {
    private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();

    private MetaElvis() {}

    @SuppressWarnings("unchecked")
    public static <T> MetaElvis<T> getInstance() { return (MetaElvis<T>) INSTANCE; }

    public void say(T t ) { System.out.println(t); }
    public void leaveTheBuilding() { System.out.println("Whoa baby, I'm outta here!"); }
         
    public static void main(String[] args) {
        MetaElvis<String> elvis1 = MetaElvis.getInstance();
        MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
        elvis1.say("hello");
        elvis2.say(100);

        // 인스턴스는 같으나 타입은 다름
        System.out.println(elvis1.equals(elvis2)); 
    }
}
  • 제네릭 활용 가능

 

public class Concert {
    private Singer singer;

    public Concert() { }
    
    public void start(Supplier<Singer> supplier) {
        Singer singer = supplier.get();
        this.singer = singer;
        singer.sing();
    }

    public static void main(String[] args) {
        Concert concert = new Concert();
        concert.start(Elvis::getInstance);
    }
}
  • 메서드 참조 사용 가능

 

예) 열거 타입

더보기

새로운 인스턴스가 생기는 일을 완벽히 막아줌

public enum AppConfig {
    INSTANCE;

    public void doSomething() {
        System.out.println("doing something");
    }
}
AppConfig a1 = AppConfig.INSTANCE;

// 직렬화
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("conf.obj"));
oos.writeObject(a1);
oos.close();

// 역직렬화
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("conf.obj"));
AppConfig a2 = (AppConfig) ois.readObject();
ois.close();

System.out.println(a1 == a2); // ✅ true!
  • 직렬화 시에도 싱글턴 보장 (JVM이 자동으로 readResolve()와 같은 효과를 보장해줌)

 

Constructor<AppConfig> cons = AppConfig.class.getDeclaredConstructor(); // ⚠️ 예외 발생
  • 리플렉션 접근이 제한됨

 

인터페이스 구현 가능

public interface Logger {
    void log(String message);
}

public enum RealLogger implements Logger {
    INSTANCE;

    public void log(String message) {
        System.out.println("[REAL] " + message);
    }
}
Logger mockLogger = Mockito.mock(Logger.class);
when(mockLogger.log("hello")).thenReturn(...);
  •  테스트 시, mock으로 분리 가능