Java/Design Pattern

[디자인 패턴] 생성패턴 - 싱글톤 패턴이란 (Singleton patterns)

민돌v 2023. 2. 8. 10:58
728x90
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 싱글톤 패턴

  • 인스턴스를 오직 한개만 제공하는 클래스

 

✔️ 싱글톤 패턴을 사용하는 이유

  • 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 객체들을 한 곳에서 제어하기 위해서 입니다.
  • 따라서 싱글톤 패턴은 2가지 목적을 가지고 있습니다.
    1. 인스턴스를 오직 1개만 만들어야한다. (한 곳에서만 제어하게)
    2. 만든 인스턴스에 글로벌하게 접근하는 방식을 제공해야한다.

 

Singleton class

 

✔️ 싱글톤 패턴을 구현하는 방법

싱글톤을 구현할 떄 신경써야하는게 크게 2가지가 있는 것 같습니다.
    1. 앱 run 시, 즉시 객체 인스턴스를 생성해 줄 것인가 (Eager initialization)
    2. 객체의 호출 시, 객체 인스턴스를 생성해 줄 것인가 (Lazy initialization)

 

1) private 생성자에 static 메소드

  • 가장 간단하고 보편적인 방법이지만, 수많은 스레드가 동시에 생성 체크(if) 문에 접근 시 객체 생성 체크를 통과하기 때문에,  멀티스레드 환경에서는 안전하지 않은 방법입니다.
  • 객체를 호출할 떄 인스턴스를 생성하는 Lazy initialization 방식입니다.
  • instance 호출 시 만들어진 instance 가 있다면 반환해주고 없다면 생성합니다.
public class SettingsPrivateStatic {
    private static SettingsPrivateStatic instance;

    private SettingsPrivateStatic() {

    }

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

 

👏🏻 다음을 직접 설명해 보세요.
1. 생성자를 private으로 만든 이유?
→ 외부에서의 객체 생성을 막고, 한 곳에서 instance 를 관리하기 위함입니다.

2. getInstance() 메소드를 static으로 선언한 이유?
→ 외부에서 글로벌하게 만들어진 객체에 접근하는 방법이 필요하기 때문입니다.

3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?
→ 객체의 생성 전, 동시 다발적으로 생성체크 박스에 스레드가 접근하게 되면, 올바른 객체 생성 체크가 이루어지지 않을 수 있기 때문입니다.

 

2) 동기화 (Synchronized)를 사용해 멀티쓰레드 환경에서도 안전한 Singleton 만들기

  • 간단하게 java 의 synchronized 를 사용하여 객체대한 접근에 lock 을 걸어 Thread Safe 하게 만드는 방법입니다.
  • 다만, 아무래도 lock 을 사용해서 순차접근을 하는 것이기 때문에 성능저하가 일어날 수 있습니다.
public class SettingsSynchronized {

    private static SettingsSynchronized instance;

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

 

👏🏻 다음을 직접 설명해 보세요.
1. 자바의 동기화 블럭 처리 방법은?
2. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?

 


 

3) 이른 초기화 (eager initialization)을 사용하는 방법

  • 멀틱쓰레드환경에서 안전한 싱글톤의 2번째 방법 입니다.
  • 아래 2가지 상황에서 유용한 방법입니다.
    1. 객체를 생성하는 비용이 적을떄
    2. 객체의 생성을 나중에(호출할 떄) 하지 않아도 될 때, 유용한 방법입니다.

 

📌 단점은 앱 구동시 미리 만든다는 것 입니다.

  • 객체의 생성비용이 큰데, 사용하지 않게된다면 앱을 구동시킬 때 너무 많은 리소스 자원을 사용하기 때문입니다.
public class SettingsEagerInitialization {
    private static SettingsEagerInitialization instance = new SettingsEagerInitialization();

    private SettingsEagerInitialization() {}

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

 

👏🏻 다음을 직접 설명해 보세요.
1. 이른 초기화가 단점이 될 수도 있는 이유?
2. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?

 

4) double checked locking으로 효율적인 동기화 블럭 만들기

  • 멀티 스레드환경에서 지연 초기화(Lazy Initialization)를 사용하고싶을 때, double checked locking 으로 효율적인 동기화 블럭을 만들 수 있습니다. 
  • if에 걸친 후, 동기화를 진행하기 때문에 위의 방식보다는 성능이점이 존재하는 방법입니다.
  • volatile 이라는 키워드를 사용해야합니다.
public class SettingsDoubleCheckSynchronized {

    private static volatile SettingsDoubleCheckSynchronized instance;

    private SettingsDoubleCheckSynchronized() {}

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

 

👏🏻 다음을 직접 설명해 보세요.
1. double check locking이라고 부르는 이유?
2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?

 

 

5) static inner 클래스를 사용하는 방법 ✨

백기선님이 권장하는 방법 중 1개라고 합니다.
  • 멀티스레드 환경에서도 안전하고, 호출될 때 instance 가 만들어지는 Lazy initialization방식입니다.
public class SettingsStaticInner {
    private static SettingsStaticInner instance;

    private SettingsStaticInner() {}

    private static class SettingHolder{

        private static final SettingsStaticInner SETTINGS = new SettingsStaticInner();
    }

    public static SettingsStaticInner getInstance() {
        return SettingHolder.SETTINGS;
    }
}

 

👏🏻 다음을 직접 설명해 보세요.
1. 이 방법은 static final를 썼는데도 왜 지연 초기화 (lazy initialization)라고 볼 수 있는가?

→ static 필드는 클래스가 처음 로딩될 때 정적인 메모리 공간에 만들어지는데, holder가 가지고 있는 클래스가 로딩되는 시점은 getInstance()를 호출할 때 로딩되기 때문에 lazy-initialization이라고 합니다

 

 

✔️ 싱글톤 패턴을 깨뜨리는 방법들

싱글톤으로 만들었음에도 불구하고 자바에서 제공하는 다양한 방법들을 이용해, 싱글톤을 깨드릴 수가 있습니다.

기본적으로 싱글톤은, 아래 그림처럼 호출할 떄 마다 같은 객체를 반환합니다.

같은 객체를 반환하는 싱글톤

 

1) 리플렉션

첫번째 방법은 리플렉션입니다.
  • 자바에서 제공하는 리플렉션 API를 사용해서 내부 설정을 변경할 수 있습니다.
  • 리플렉션으로 생성자를 가져와, 접근제어자에 대한 설정을 변경해 Singleton으로 구현한 객체를 새로운 인스턴스로 생성할 수 있는 방법입니다.
@Test
@DisplayName("리플렉션을 사용해서 Singleton 깨드리기")
void reflection() throws Exception{

    SettingsStaticInner setting = SettingsStaticInner.getInstance();

    Constructor<SettingsStaticInner> declaredConstructor = SettingsStaticInner.class.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);

    SettingsStaticInner setting1 = declaredConstructor.newInstance();

    assertThat(setting).isEqualTo(setting1);
}

Class.getDeclaredConstructor( ) : 해당 Class 객체가 나타내는 클래스 또는 인터페이스의 지정된 생성자를 반영하는 Constructor 객체를 반환합니다. 

Constructor.setAccessible(true) : 해당 객체에 대해 Java 언어 액세스 제어에 대한 검사를 억제해야함을 나타냅니다. 즉 접근제어를 무시함을 나타냅니다. (false) 일 경우 접근제어의 억제를 하지 않겠다는 의미를 가집니다.

Constructor.newInstance( ) : 이 Constructor 개체가 나타내는 생성자를 사용하여 지정된 초기화 매개 변수를 사용하여 생성자의 선언 클래스의 새 인스턴스를 만들고 초기화합니다.

이렇게 리플렉션을 이용하여, 해당 객체의 생성자를 불러오고 → 접근제어를 풀고 → 새로운 인스턴스를 생성하여 싱글톤 객체의 제어에서 벗어날 수 있습니다.

 

👏🏻 다음을 직접 설명해 보세요.
1. 리플렉션에 대해 설명하세요.
→  구체적인 클래스 타입을 알지 못해도, 클래스에 대한 메서드, 타입, 변수들을 접근할 수 있도록 해주는 Java API입니다.

2. setAccessible(true)를 사용하는 이유는?
→ 필드나 메서드의 접근제어 지시자에 대한 제어를 변경하기 위한 메소드입니다. 즉 private으로 제어한 접근 설정에 대해 접근 권한을 주는 행위입니다.

 


2) 직렬화 및 역직렬화의 사용

두번째 방법은, 객체를 직렬화시킨 후 다시 역직렬화 시켜서 새로운 객체를 만드는 방법입니다.
  • 자바에서 외부 파일을 역직렬화 할 시에는, 반드시 생성자를 사용하여 새로운 인스턴스를 만들어 줍니다.
  • 이러한 설정을 이용해서, Singleton 설정을 깨뜨릴 수 있습니다.
@Test
@DisplayName("직렬화 후 역직렬화로 singleton 깨뜨리기")
void serializerAndDeSerializer() throws IOException, ClassNotFoundException {
    SettingsStaticInner setting = SettingsStaticInner.getInstance();
    SettingsStaticInner setting1 = null;

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

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

    assertThat(setting).isEqualTo(setting1);
}

👏🏻 이렇게 직렬화를 할 떄 직렬화할 객체에 Implements Serializable 을 선언해주지 않으면 아래 에러로그를 만나게 됩니다.

  • java.io.NotSerializableException: com.example.patterns._01_singleton.SettingsStaticInner

역직렬화시 다른 객체를 반환

 

 

📌 대응방법

  • 역직렬화 시 생성자 사용 문제대한 해결방안은, "readResolve( )" 메소드를 객체 안에 재정의 해 주는 것입니다.
  • ObjectInputStream 에 대한 공식문서를 살펴보면, "역직렬화된 개체는 다른 당사자에게 표시되는 개체를 반환하는 readResolve 메서드를 정의하거나 readUnshared가 스트림의 다른 곳에서 또는 외부 수단을 통해 얻을 수 있는 클래스 개체 또는 열거형 상수를 반환할 수 있습니다." 라고 나옵니다.
  • 역직렬화 시 readObject보다 readResolve 메소드를 먼저 호출하게 되어있기 때문에 만들어 둔 instance를 반환하도록 해주면 된다고 합니다. 
public class SettingsStaticInner implements Serializable {

    private static SettingsStaticInner instance;

    private SettingsStaticInner() {}

    private static class SettingHolder{

        private static final SettingsStaticInner SETTINGS = new SettingsStaticInner();
    }

    public static SettingsStaticInner getInstance() {
        return SettingHolder.SETTINGS;
    }

    private Object readResolve() {
        return SettingHolder.SETTINGS;
    }
}

역직렬화시에도 같은 객체를 반환

 

👏🏻 다음을 직접 설명해 보세요.

1. 자바의 직렬화 & 역직렬화에 대해 설명하세요.
 

2. SerializableId란 무엇이며 왜 쓰는가?
 

3. try-resource 블럭에 대해 설명하세요.
 

 


3) 리플렉션의 대응방법 - Enum Singleton

  • enum을 사용한다면 리플렉션에 대응할 수 있습니다.
  • 내부 바이트코드를 보게되면  enum class는 리플렉션에 대해서도 막아놓았기 때문에 안전하다고 합니다.
  • 다만 Enum은 앱 로딩시 메모리에 할당되기 때문에 지연 초기화는 할 수 없습니다.
public enum Settings {
 	INSTANCE;
}

 

👏🏻 다음을 직접 설명해 보세요.

1. enum 타입의 인스턴스를 리팩토링을 만들 수 있는가?
  

2. enum으로 싱글톤 타입을 구현할 때의 단점은?
 

3. 직렬화 & 역직렬화 시에 별도로 구현해야 하는 메소드가 있는가?
 

 

 

✔️ 싱글톤 (Singleton) 패턴 복습

• 자바에서 enum을 사용하지 않고 싱글톤 패턴을 구현하는 방법은?

• private 생성자와 static 메소드를 사용하는 방법의 단점은?

• enum을 사용해 싱글톤 패턴을 구현하는 방법의 장점과 단점은?

• static inner 클래스를 사용해 싱글톤 패턴을 구현하라.

 

✔️ 싱글톤 (Singleton) 패턴 실무에서는 어떻게 쓰이나?

 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프.

• 자바 java.lang.Runtime

• 다른 디자인 패턴(빌더, 퍼사드, 추상 팩토리 등) 구현체의 일부로 쓰이기도 한다

 

 

 

반응형