[Java Design Pattern] 자바 디자인패턴, Singleton 싱글톤 패턴

싱글톤 패턴

시스템 런타임이나 환경 셋팅에 대한 정보등 클래스 인스턴스가 여러개일 때 문제가 발생할 수 있는 요구사항이 있다. 이럴 경우 싱글톤 패턴을 활용한다면 인스턴스를 오직 한개만 만들어 제공할 수 있다.

 

우선 가장 기본적인 싱글톤 예제를 보자.

public class Settings {
    private static Settingsinstance;

    private Settings() { }

    public static Settings getInstance() {
        if (instance== null) {
            instance= new Settings();
        }
        return instance;
    }
}

싱글톤 구현 방법중에서 이러한 예제를 가장 많이 보았을 것 같다. 하지만 이 예제는 멀티스레드 환경을 전혀 고려하지 못한 싱글톤 예제이다.

 

코드를 한번 바꿔보자.

public class Setting {
    private static Setting instance;

    private Setting() { }

    public static Setting getInstance() throws Exception {
        if (instance== null) {
            Thread.sleep(1000); // 1초 동안 무언가 엄청 많은 작업을 한다.
            instance= new Setting();
        }
        return instance;
    }
}

이런 경우가 됐을때, 멀티스레드 환경에서 해당 코드를 호출하면 어떻게 될까? 2개의 스레드가 if문을 모두 통과하여 결과적으로 마지막으로 수행된 new 연산자에 의해 instance는 변경이 될것이고 결과적으로 heap에는 2개의 인스턴스가 쌓였을 것이다.(결국 하나는 garbage가 되겠지만..)

 

우선 이러한 멀티스레드 환경에서의 문제점을 초점으로 개선해보자.

 

멀티스레드 환경이니까 동기화를 해주면 어떨까?

public class Setting {
    private static Setting instance;

    private Setting() { }

    public static synchronized Setting getInstance() throws Exception {
        if (instance== null) {
            Thread.sleep(1000); // 1초 동안 무언가 엄청 많은 작업을 한다.
            instance= new Setting();
        }
        return instance;
    }
}

이런식으로 메소드에 synchronized 키워드를 붙여 스레드가 해당 메소드에 접근할때마다 메소드 접근을 순차적으로 접근하도록 하면 되지 않을까?

 

하지만 이는 멀티스레드 환경에서의 문제는 개선했지만 동기화 작업에 대한 부담만 다소 가중시킬뿐 근본적인 해결이 될 수는 없다. 그냥 딱 그 순간에 한번만 호출되면 되고 나머지 메소드 호출에서는 동기화 작업이 필요 없으니까 말이다.

 

그럼 내부적으로 클래스 초기화시에 나의 단 하나의 인스턴스를 초기화해서 저장해놓으면 문제는 해결되지 않을까?

코드로 보자.

public class Setting {
    private static Setting instance = new Setting();

    private Setting() { }

    public static Setting getInstance(){
        return instance;
    }
}

이런식으로 클래스의 인스턴스에 접근할때가 아니라 개발자가 접근하기 이전부터 가지고 있으면 되지 않을까?(이른 초기화, Eager Initialization)

 

사실 가벼운 실무에서는 이러한 내용이 맞고 적절히 넘어갈 수도 있다고 본다. 하지만 private 생성자 안쪽 코드를 비워놔서 괜찮지 인스턴스를 미리 생성해둔다는 것은 사용하지 않게되면 불필요한 메모리가 heap에 쌓여 있다고 볼 수도 있다.

 

그리고 소프트웨어를 다루는 직업이라면 좀더 개념에 맞는 이치에 맞는 타이밍에 정확히 해당 코드가 싱글톤 객체를 생성해서 정확한 타이밍에 만들어주어야 하지 않을까? 싶기도 하다.

 

결국은 정확한 그 타이밍에 동기화가 적용되어 순차성을 보장해야만하며 싱글톤 인스턴스가 생성된 이후로는 동기화 작업은 진행하지 않고 생성된 인스턴스를 담은 변수에만 접근하여 싱글턴 인스턴스를 가져오면 딱 좋겠다!

 

코드로 보자.

public class Setting {
    private static volatile Setting instance;

    public static Setting getInstance() {
        if (instance== null) {
            synchronized (Setting.class) {
                if (instance== null) {
                    instance= new Setting();
                }
            }
        }
        return instance;
    }
}

 

해당 코드를 보면 동기화는 Setting 클래스에 해당하는 타입에 대해서만 진행하며 두번의 instance에 대한 null 체크가 존재한다. 이를 double checked locking이라고 하는데, 첫번째 if문은 빠르게 통과할 것이고, 동기화 블럭은 싱글턴 인스턴스를 생성해주는 블럭이기 때문에 동기화 순차성에 의해 1번만 호출될 것이다. 그러기 위해서 두번째 if문이 있다. 해당 코드가 자연스럽게 instance 변수를 통해서 리턴될 수 있도록 디자인되어있다.

 

하지만 아래의 키워드가 되게 낯설다.

volatile

해당 키워드는 java 1.5부터 동작하게 될텐데 이에 대한 내용은 여기를 참조하면 좋을 것 같다.

 

그런데 말입니다.

 

단순히 프로그래밍 언어를 이용해서 개발을 하는데 이거 이렇게 너무 컴퓨터의 메모리 구조까지 알아가면서 개발해야될 내용인가? 싶을 것 같다.

 

좀더 쉬운방법은 없나?

코드부터 보자

public class Setting {

    private Setting() { }

    private static class SettingHolder {
        private static final Setting INSTANCE = new Setting();
    }

    public static Setting getInstance() {
        return SettingHolder.INSTANCE;
    }
}

해당 방식은 필자가 많이 사용하는 싱글턴 패턴 구현방식이다. 아래의 내용을 보자.

- static inner class를 활용하여 해당 SettingHoler 클래스에 접근하지 않는 경우 INSTANCE 변수 및 클래스 정보는 아직 초기화되지 않습니다.

- 개발자가 getInstance()를 최초 호출하는 경우 SettingHolder 클래스가 로드되며 그 안쪽의 INSTANCE 변수 또한 초기화되어 싱글톤 인스턴스가 채워집니다.

- 이후로 호출하는 경우는 INSTANCE 변수에 접근하는 것일 뿐 new Setting() 코드는 더이상 동작하지 않기 때문에 싱글턴을 유지할 수 있습니다.

 

리플렉션 이슈

싱글톤을 깨버리는 가장 심각한 이슈가 리플렉션이다.

아래의 코드를 보자

class Main {
    public static void main(String[] args) throws Exception {
        Constructor<Setting> declaredConstructor = Setting.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Setting setting1 = declaredConstructor.newInstance();
        Setting setting2 = Setting.getInstance();
        Setting setting3 = Setting.getInstance();
    
        System.out.println(setting1 == setting2); // false
        System.out.println(setting2 == setting3); // true
    }
}

위 코드는 리플렉션을 활용하여 Setting 클래스의 생성자에 접근하고 private으로 선언된 생성자의 접근 가능성을 true로 변경하여 직접 생성자를 호출하여 인스턴스를 만들어보는 코드이다.

 

이처럼 자바에서는 리플렉션이라는 강력한 클래스 정보 접근 기술이 있기 때문에 해당 기술을 활용하지 않는 한 싱글톤 패턴은 유지될 수 있다. 사실 이렇게 복잡한 방법으로 인스턴스를 생성하는 개발방식은 거의 모든 실무에서 사용하지 않겠지만 결과적으로 방법은 열려 있다는 것은 변하지 않는다.

 

그럼 이를 해결할 순 없을까?

 

정답은 enum을 활용하면 된다.

public enum Setting {
    INSTANCE;
}

이런식으로 Enum으로 쓰게되면 리플렉션 기술을 이용하더라도 생성자는 호출할 수 없다. 그런데 enum은 말그대로 상수이기 때문에 사용하기 조금 난해한 경우가 있을 것 같았다. -> 가변하는 값의 경우에는 어쩔 도리가 없다. 싱글톤이랑 상수랑은 엄연히 다른 개념이기 때문이다.

 

따라서 필자라면 static inner class를 활용하지 않을까 싶다. 

댓글

Designed by JB FACTORY