[객체 생성 패턴] Chapter 1-3. Singleton Pattern : 싱글톤을 깨트리는 방법

 

✍️ 싱글톤을 깨트리는 방법

자바에서 허용하는 문법을 이용하면 싱글톤을 깨트릴 수 있다.

 

아래 코드에서 몇 가지 자바 문법을 사용하면 settings1과 settings2의 비교 결과가 false가 될 수 있다.

public class Settings {
    private Settings() {
    }

    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.INSTANCE;
    }
}
public class App {
    public static void main(String[] args) {
        Settings settings1 = Settings.getInstance();
        Settings settings2 = Settings.getInstance();

        System.out.println(settings1 == settings2);
    }
}

// true

 

1, 리플렉션

자바의 리플렉션을 이용하면 클래스의 생성자를 받아올 수 있고, 받아온 생성자의 newInstance를 통해 인스턴스를 생성할 수 있다. 단, 여기서 생성된 인스턴스는 Holder가 가지고 있는 인스턴스와는 전혀 다른 새로운 인스턴스다.

public class App {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings settings1 = Settings.getInstance();

        Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        Settings settings2 = constructor.newInstance();

        System.out.println(settings1 == settings2);
    }
}

// false

 

2-1, 직렬화 & 역직렬화

직렬화란 오브젝트를 파일 형태로 디스크에 저장하는 것이다. 반대로 다시 읽어들이는 것을 역직렬화라고 한다.

기본적으로 Serializable 인터페이스를 구현한 클래스의 인스턴스들은 직렬화와 역직렬화에 사용할 수 있다. 이 말인즉슨 인스턴스를 파일로 저장했다가 다시 읽어올 수 있다는 뜻이다.

 

직렬화

public class App {
    public static void main(String[] args) throws Exception {
        Settings settings1 = Settings.getInstance();

        try(ObjectOutput output = new ObjectOutputStream(new FileOutputStream("settings.obj"))){
            output.writeObject(settings1);
        }
    }
}
// settings.obj

�� sr Singleton.Settings����q�  xp

 

역직렬화

public class App {
    public static void main(String[] args) throws Exception {
        Settings settings1 = Settings.getInstance();

        try(ObjectOutput output = new ObjectOutputStream(new FileOutputStream("settings.obj"))){
            output.writeObject(settings1);
        }

        Settings settings2 = null;

        try(ObjectInput input = new ObjectInputStream(new FileInputStream("settings.obj"))){
            settings2 = (Settings) input.readObject();
        }

        System.out.println(settings1 == settings2);
    }
}

 

역직렬화를 할 때 반드시 생성자를 통해 다시 한번 인스턴스를 만들기 때문에 직렬화에 사용한 인스턴스와는 전혀 다른 인스턴스가 된다. 

 

2-2, 역직렬화 대응 방안

역직렬화의 대응 방안으로 readResolve가 있다. readResolve를 정의하면 readObject를 통해 만들어진 인스턴스 대신 readResolve가 반환하는 인스턴스가 역직렬화의 결과물이 된다.

public class Settings implements Serializable {
    private Settings() {
    }

    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.INSTANCE;
    }

    protected Object readResolve(){
        return SettingsHolder.INSTANCE;
    }
}

 

🍊 리플렉션을 막을 순 없을까?

직렬화 & 역직렬화는 readResolve를 구현함으로써 대응할 수 있었지만, 지금까지의 싱글톤 구현 방법으론 리플렉션에 대응하진 못한다. 

 

그렇다면 리플렉션은 절대 방어하지 못하는 걸까?