본문 바로가기
Computer Sience/Java

[Effective Java] 아이템3: private 생성자나 열거 타입으로 싱글턴임을 보증하라

by 제우제우 2024. 10. 25.

Chapter 02: 객체 생성과 파괴

객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법

올바른 객체 생성 방법에 대한 설명

싱글톤 패턴 

이펙티브 자바 아이템3 에서 나오는 대부분의 내용을 정리한 적 있었다. 
이번엔 복습 차원으로 정리하지 않았던 내용에 대해서만 간단하게 정리하겠다. 

 

해당 링크에는 지금 내용에 없는 내용들이 많다.

 

1. 직렬화시 Serializable 선언과 주의점 

2. 멀티 스레딩에 안전한 여러 싱글톤 생성 방법

3. 이른 초기화 방식과 지연 초기화 방식 

4. enum 

 

디자인 패턴: 싱글톤 패턴 정리

 

[Design Pattern] 싱글톤(Singleton) 패턴

싱글톤 (Singleton) 패턴인스턴스를 오직 한개만 제공하는 클래스 싱글톤(Singleton) 패턴은 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 그 인스턴스에 대한 전역 접근을 제공하

20240228.tistory.com

 

싱글턴이란?

인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 

싱글톤 패턴1

public class Singleton1 {
    public static final Singleton1 INSATANCE = new Singleton1();
    private Singleton1(){
    }
}

 

public class Client {
    public static void main(String[] args) {
        // Singleton1 singleton1 = new Singleton1(); 인스턴스 생성 불가
        Singleton1 instance1 = Singleton1.INSTANCE;
        Singleton1 instance2 = Singleton1.INSTANCE;
        System.out.println(instance1 == instance2); // true
    }
}

 

private 생성자는 public static final 필드인 Singleton1.INSTANCE를 초기화할 때 딱 한번만 호출된다. 

Singleton1 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다. 

 

하자만 1가지 예외가 있다. 

→ 리플렉션 

 

리플렉션을 사용한 공격 

public class Client {
    public static void main(String[] args) {
        // Singleton1 singleton1 = new Singleton1(); 인스턴스 생성 불가
        Singleton1 instance1 = Singleton1.INSTANCE;
        Singleton1 instance2 = Singleton1.INSTANCE;
        System.out.println(instance1 == instance2); // true

        Singleton1 instance3 = null;
        Constructor<?>[] constructors = Singleton1.class.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            try {
                constructor.setAccessible(true);
                instance3 = (Singleton1) constructor.newInstance();
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
        System.out.println(instance1 != instance3); // true
    }
}

constructor (생성자)를 가져와서 setAccessible(true) API를 사용하면
접근제어자가 private 이어도 이렇게 런타임에 접근이 가능하게 된다.

 

이를 막는 방법

public class Singleton1 {
    public static final Singleton1 INSTANCE = new Singleton1();
    private static int count;
    public static int getCount(){
        return count;
    }
    private Singleton1(){
        count++;
        if(count > 1){
            throw new IllegalStateException("this object should be singleton");
        }
    }
}

싱글톤 패턴2

public class Singleton2 {
    private static final Singleton2 INSTANCE = new Singleton2();
    private Singleton2(){
    }
    public static Singleton2 getInstance(){
        return INSTANCE;
        // return new Singleton2();
    }
}

 

이전 코드와 거의 동일하지만 INSTANCE를 private으로 외부에서 접근을 막고 

정적 팩토리 방식의 메소드 getInstance()를 추가했다. 

해당 방식의 장점은 나중에 마음이 바뀌어서 싱글톤 → not 싱글톤  바뀌어도 클라이언트가 사용하는 API는 동일하다는 것이다.

// 정책(싱글톤 -> not 싱글톤)이 바뀌어도 API 동일하다.
Singleton2 instance1 = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();

 

정적 팩토리의 메소드 참조를 supplier(공급자)로 사용할 수 있다.

Supplier<Singleton2> supplier = Singleton2::getInstance;

 

싱글톤 패턴3 - 직렬화 

public class Singleton3 implements Serializable {
    private static final Singleton3 INSTANCE = new Singleton3();
    private Singleton3() {
    }
    public static Singleton3 getInstance(){
        return INSTANCE;
    }
}

 

마커 인터페이스인 Serializable 인터페이스를 구현 

public class Client {
    public static void main(String[] args) {
        Singleton3 instance1 = Singleton3.getInstance();

        try(ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton"))){
            out.writeObject(instance1);
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
        Singleton3 instance2 = null;
        try(ObjectInput in = new ObjectInputStream(new FileInputStream("singleton"))) {
            instance2 = (Singleton3) in.readObject();
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
        System.out.println(instance1 != instance2);
    }
}

출력을 보면 true로 나온다.

즉, 싱글톤이 깨진다. 

 

readResolve() 메소드 추가 

public class Singleton3 implements Serializable {
    private static final Singleton3 INSTANCE = new Singleton3();
    private Singleton3() {
    }
    public static Singleton3 getInstance(){
        return INSTANCE;
    }
    protected  Object readResolve(){
        return getInstance();
    }
}

 

다시 돌리면 이제 false가 나온다. 

즉, 싱글톤이 지켜진다.

 

모든 인스턴스 필드를 transient 선언하자. 

readResolve()에서 INSTANCE를 리턴하기 때문에 역직렬화한 필드들이 의미가 없다. 

그러니 불필요한 직렬화를 줄이는 최적화 transient 키워드를 붙이자. 

 

참고로 예제코드에 있는 readResolve() 같이 메소드가 있으면 역직렬화된 객체는 가비지 컬렉션 대상이 된다.  

싱글톤 패턴4 - enum

public enum Singleton4 {
    INSTANCE
}

 

상속 구조: enum → extends abstract class Enum → implements Serializable 

 

직렬화 & 리플렉션에 안전

public class Client {
    public static void main(String[] args) {
        Singleton4 instance1 = Singleton4.INSTANCE;
        Singleton4 instance2 = null;

        // 리플렉션 불가능
        Constructor<?>[] constructors = Singleton4.class.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            try {
                constructor.setAccessible(true);
                instance2 = (Singleton4) constructor.newInstance();
            }
            catch (Exception e){
                e.printStackTrace();
                // java.lang.IllegalArgumentException: Cannot reflectively create enum objects
            }
        }
        System.out.println(instance2 == null); // true

        // 직렬화에 안전
        try(ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton"))) {
            out.writeObject(instance1);
        }
        catch (Exception e){
            e.printStackTrace();
        }
        Singleton4 instance3 = null;
        try(ObjectInput in = new ObjectInputStream(new FileInputStream("singleton"))) {
           instance3 = (Singleton4) in.readObject();
        }
        catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(instance1 == instance3);
    }
}