본문 바로가기

개발/Java

[Java] Design-Pattern : Singleton(1)

개발을 진행하면서 디자인 패턴에 대해 많이 듣게 된다.

특히 스프링은 컨테이너가 빈을 "싱글톤"으로 등록하여 사용한다고 하는데 지금까지는

어떠한 방법으로 싱글톤 패턴을 사용할 수 있는지, 그렇게 함으로 얻는 장점은 무엇인지 깊게 고민해보지 못한 듯 한다.

 

싱글톤 패턴은 객체가 오직 1개만 생성이 되는 패턴을 의미한다.


우리가 일반적으로 객체를 생성하는 방법은 new 연산자를 통해 할 수 있다.

물론 이렇게 생성한 두 객체는 동일하지 않다.

public class App {
    public static void main(String[] args) {
        Settings settings = new Settings();
        Settings settings1 = new Settings();

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

가장 기본적으로 객체를 하나만 생성하는 방법은 객체의 생성자를 private으로 선언하여,

new 연산자로 생성할 수 없도록 하고, static 메서드로 인스턴스를 가져오도록 한다.

또한 동일한 객체를 보장하기 위해, 객체를 전역변수로 선언하고, null 체크를 통해 동일 객체로 생성한다.

public class Settings {

    private static Settings instance;

    private Settings() {} // 생성자 제한

    // 동일 객체 생성 보장
    public static Settings getInstance() { 
        if(instance == null) {
            instance = new Settings();
        }
        return instance;
    }
}
public class App {
    public static void main(String[] args) {
//        Settings settings = new Settings(); // new 사용 불가
        Settings settings = Settings.getInstance();
        System.out.println(settings  == Settings.getInstance()); // true
    }
}

하지만 위의 케이스도 멀티 스레드 환경에서는 동일 객체를 보장하지 못한다.

Thread Safe한 가장 간단한 방법으로는 객체 생성 메서드를 synchronized 로 동기화 처리 하는 것이다.

public class Settings {

    private static Settings instance;

    private Settings() {}

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

synchronized 는 성능상 이슈가 있을 수 있다. 매번 객체 생성 호출시마다, Lock 에 걸릴 위험이 있다.

객체 생성의 비용이 크지 않다면, "이른 초기화(Eagal initialization)"를 통해 미리 생성해두고 객체를 호출하는 방법도 있다.

public class Settings {

    private static final Settings INSTANCE = new Settings();

    private Settings() {}

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

하지만 위의 방법도 이른 초기화로 인해, 불필요한 객체를 미리 생성하게 될 수도 있고,

생성 비용이 큰 객체라면, 성능 저하를 유발하게 될 수도 있다.

객체가 null일 경우에만 동기화를 적용해보자.(Double checked locking)

public class Settings {

    private static volatile Settings instance;

    private Settings() {}

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

위 방법은 이른 초기화와 멀티 스레드 환경에서의 문제를 모두 잡기는 했으나,

코드가 복잡하고 volatile 키워드로 인해 자바 버전의 제한도 생긴다. (1.5 이상)

이를 해결하기 위해 static inner class를 사용한다. (권장됨)

public class Settings {

    private Settings() {}

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

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

이 코드에서는 getInstance() 메서드가 호출될 때 static 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