본문 바로가기

개발/Java

[Java] Design-Pattern : Singleton(2)

이전 포스트에서 싱글턴 객체를 생성하는 기본적인 방법과,

멀티 스레드 환경에서 동시성 이슈에 대응하여 싱글턴을 생성하는 방법에 대해 알아보았다.

 

하지만 이렇게만 하여도 완벽하게 동일한 객체를 생성하였다고 할 수 있을까?

기존에 생성한 싱글턴 구현을 깨는 방법과 그 방법들을 막는 방안에 대해 살펴보자.


 

직렬화, 역직렬화를 통한 객체 생성

먼저 Settings 객체에 직렬화 인터페이스를 구현한다.

public class Settings implements Serializable {}
public class App {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Settings settings = Settings.getInstance();
        Settings settings1 = null;

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

        try (ObjectInput in = new ObjectInputStream(new FileInputStream("setting.obj"))) {
            settings1 = (Settings) in.readObject();
        }

        System.out.println(settings == settings1); //false
    }
}

이러한 직렬화, 역직렬화 방식은 Settings 내에 메소드 오버라이딩으로 해결이 가능하다.

public class Settings implements Serializable {
    //생략
    //추가
    protected Object readResolve() {
        return getInstance();
    }
}

Reflection 사용

자바의 리플렉션 기능을 사용하면 private 생성자로 생성을 방지하였음에도 불구하고 새로운 객체 생성이 가능하다.

public class App {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings settings = Settings.getInstance();
        Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings settings1 = constructor.newInstance();

        System.out.println(settings == settings1); //false
    }
}

이 경우 Settings를 Enum으로 생성하면 해결이 가능하다.

public enum Settings {
    INSTANCE;
}
public class App {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings settings = Settings.INSTANCE;
        Settings settings1 = null;
        Constructor<?>[] declaredConstructors = Settings.class.getDeclaredConstructors();

        for (Constructor<?> constructor : declaredConstructors) {
            constructor.setAccessible(true);
            settings1 = (Settings) constructor.newInstance("INSTANCE");
        }
    }
}

애초에 newInstance 에서 예외가 발생한다.

 

또한 Enum은 기본적으로 Serializable 인터페이스를 구현하고 있는데, 이 부분 또한 방어가 되어 있다.

public class App {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Settings settings = Settings.INSTANCE;
        Settings settings1 = null;
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
            out.writeObject(settings);
        }
        try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
            settings1 = (Settings) in.readObject();
        }
        System.out.println(settings == settings1); //true
    }
}

이렇게 보면 Enum으로 싱글톤을 구현하는것이 완벽해보이지만..

아쉽게도 이 방법에도 한계가 존재한다.

  • Enum은 클래스가 로드되는 시점에 미리 생성이 된다. Lazy 로딩 할 수 없다.
  • 개발자가 상속을 사용해야 하는 객체에 대해서는 적용할 수 없다.

따라서 이러한 상황에서 싱글턴을 생성하고자 한다면, inner class 방식을 사용하는 것이 좋다.


참고 : 인프런, 백기선, 코딩으로 학습하는 GoF의 디자인 패턴(https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4)

 

코딩으로 학습하는 GoF의 디자인 패턴 강의 - 인프런

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com