코딩으로 학습하는 GoF의 디자인 패턴 (인프런) - 싱글톤 패턴

김민혁·2023년 1월 12일
0

이 게시글은 코딩으로 학습하는 GoF의 디자인 패턴 (인프런 - 백기선)
강의를 듣고 정리한 내용을 바탕으로 작성되었습니다.

싱글톤 패턴이란?

싱글톤 패턴은 객체의 생성과 관련한 패턴으로 객체를 단 하나만 생성하여 전역적으로 제공하는 패턴이다.

가장 간단하게 싱글톤 패턴을 구현하는 방법

  • Settings
public class Settings {

    private static Settings instance;

	private Settings() {}

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

    @Test
    @DisplayName("getInstance를 2번 호출해도 같은 객체를 반환한다.")
    void getInstance() {
        Settings instance1 = Settings.getInstance();
        Settings instance2 = Settings.getInstance();
		// 통과
        assertTrue(instance1 == instance2);
    }

다음과 같은 코드로 간단하게 싱글톤 패턴을 구현할 수 있다.
하지만 해당 구현은 멀티 쓰레드 상황에서는 객체의 동일성이 보장되지 않을 수 있다. (Thread-safe 하지 않다)

객체 동일성이 보장되지 않는 시나리오는 다음과 같다.

  1. 처음 메소드를 호출한 쓰레드 A가 if문을 통과한다.
  2. 쓰레드 A가 new로 객체를 생성하여 instance 필드에 할당하기 전에 쓰레드 B가 메소드를 호출하여 if문 조건을 true로 통과한다.
  3. 쓰레드 A와 B가 호출한 메소드가 각자 다른 참조값을 가진다.
  • Settings2
public class Settings2 {

    private static Settings2 instance;

    private Settings2() {
    }

    public static Settings2 getInstance() {
        if (instance == null) {
            instance = new Settings2();
        }
        return instance;
    }
}
  • Test Code

	/**     출력 결과
     *      singleton.Settings@48b338ca
     *      singleton.Settings@768f4284
     * */
    @Test
    @DisplayName("멀티 쓰레드는 환경에서 다른 참조값을 가질수 있다.")
    void getInstance2() throws InterruptedException {
        Thread t1 = new TestThread();
        Thread t2 = new TestThread();

        t1.start();
        t2.start();

        // t1 , t2가 종료되기 전에 테스트를 진행하는 쓰레드가 종료되지 않게 지연함
        Thread.sleep(100L);
    }
class TestThread extends Thread {

    @Override
    public void run() {
        Settings2 instance = Settings2.getInstance();
        System.out.println(instance);
    }
}

Thread-safe 하게 싱글톤을 구현하는 방법

1. synchronized 사용

public class Settings3 {

    private static Settings3 instance;

    private Settings3() {
    }

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

synchronized로 쓰레드를 동시에 접근하지 못하게 제한하여 싱글톤을 보장할 수 있다.
단점 : 모든 경우에 쓰레드를 block하기 때문에 성능이 좋지 못하다.

  • Test code
   /**
     *   출력 결과 -> 참조값이 동일하다.
     *   singleton.Settings3@5e4d06df
     *   singleton.Settings3@5e4d06df
     *   singleton.Settings3@5e4d06df
     *   singleton.Settings3@5e4d06df
     *   singleton.Settings3@5e4d06df
     * */
    @Test
    @DisplayName("synchronized 키워드로 싱글톤을 보장한다.")
    void getInstance3() throws InterruptedException {
        Thread t1 = new TestThread2();
        Thread t2 = new TestThread2();
        Thread t3 = new TestThread2();
        Thread t4 = new TestThread2();
        Thread t5 = new TestThread2();

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        Thread.sleep(100L);
    }

2. double checking locking

public class Settings4 {

    private static Settings4 instance;

    private Settings4() {}

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

해당 구현은 instance가 null인 경우에만 쓰레드를 block 하기 때문에 성능적으로 1번보다 조금 더 보완되었다.

3. static field 초기화 (eager initialization)

public class Settings5 {

    private static final Settings5 INSTANCE = new Settings5();

    private Settings5() {
    }

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

static 필드로 초기화하여 클래스가 로딩되는 시점에 객체를 생성하여 제공하며 싱글톤을 보장할 수 있다.
단점 : 해당 방법은 클래스 로드 시점에 객체를 생성하는 리소스가 소모된다.

4. static inner class (Holder) - 권장됨

public class Settings6 {

    private Settings6() {
    }

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

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

static inner class를 사용하여 객체를 제공하면 getInstance() 가 호출되는 시점에 객체를 생성하여 (지연로딩) 제공할 수 있다.

5. enum 사용

public enum SettingEnum {
    INSTANCE;
}

가장 간단하게 enum을 사용하여 싱글톤을 구현할 수 있다.

사용하는 측에서 싱글톤의 객체 동일성을 깨뜨리는 방법

public class BreakSingletonTest {

    @Test
    @DisplayName("1. 리플렉션을 사용하여 객체를 생성하여 객체 동일성을 깨뜨린다.")
    void break1() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings6 settingsA = Settings6.getInstance();

        Constructor<Settings6> constructor = Settings6.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings6 settingsB = constructor.newInstance();

        // 통과 -> 객체 참조값이 다름
        assertTrue(settingsA != settingsB);
    }

    @Test
    @DisplayName("2. 객체 직렬화 & 역직렬화를 통해 객체 동일성을 깨뜨린다.")
    void break2() {
        Settings7 settingsA = Settings7.getInstance();

        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.out"))) {
            out.writeObject(settingsA);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        Settings7 settingsB = null;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.out"))) {
            settingsB = (Settings7) in.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // 통과 -> 객체 참조값이 다름
        assertTrue(settingsA != settingsB);
    }

    // enum은 리플렉션으로 생성할 수 없고 IllegalArgumentException를 던진다.

    @Test
    @DisplayName("3. Enum을 리플렉션으로 생성하기 - 생성자 파라미터 1개")
    void break3() {
        Constructor<?>[] constructors = SettingEnum.class.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            constructor.setAccessible(true);
            Assertions.assertThrows(IllegalArgumentException.class, () -> {
                SettingEnum instance = (SettingEnum) constructor.newInstance("INSTANCE");
            });
        }
    }


    // ENUM은 기본 생성자가 존재하지 않고 name을 인자로 가지는 생성자가 존재하기 때문에
    // NoSuchMethodException를 던진다.
    @Test
    @DisplayName("4. Enum을 리플렉션으로 생성하기 - 생성자 파라미터 0개")
    void break4(){
        Assertions.assertThrows(NoSuchMethodException.class, () -> {
            Constructor<SettingEnum> declaredConstructor = SettingEnum.class.getDeclaredConstructor();
        });
    }

    // enum은 직렬화 & 역직렬화시에도 동일성을 보장한다.
    @Test
    @DisplayName("5. 객체 직렬화 & 역직렬화를 통해 객체 동일성을 깨뜨린다.")
    void break5() {
        SettingEnum settingsA = SettingEnum.INSTANCE;

        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.out"))) {
            out.writeObject(settingsA);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        SettingEnum settingsB = null;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.out"))) {
            settingsB = (SettingEnum) in.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // 통과 -> 객체 참조값이 같음
        assertTrue(settingsA == settingsB);
    }

-> 객체 직렬화 & 역 직렬화 방법을 막으려면 readResolve 메소드를 명시적으로 오버라이드 한다. 참조

-> Reflection으로 객체를 생성하는 것을 막으려면 enum으로 싱글톤을 구현해야 한다.

enum은....

  • enum의 바이트코드

    바이트 코드를 열어보면 enum은 java.lang.Enum을 상속 받고있고 Enum은 Serializable을 구현하고 있다.

  • newInstance 내부에서 enum의 객체화를 막아주고 있다.

싱글톤 패턴은 안티패턴이다?!?

생각해봅시다...

profile
안녕하세요 김민혁입니다.

0개의 댓글