Joonas' Note

Joonas' Note

[JAVA] 싱글톤 패턴 (Singleton Pattern) 본문

개발/Java

[JAVA] 싱글톤 패턴 (Singleton Pattern)

2022. 4. 11. 19:01 joonas

    작년쯤에 Devlog에 정리한 적이 있지만, GitBook 에디터의 한글 타이핑 버그가 너무 심각해서 옮길 겸 다시 정리한다.

     

    Singleton Pattern - Today Joonas Learned

    Config 클래스의 생성자를 직접 호출하지 못하도록 하여, 인스턴스가 한번만 생성되도록 한다. 대신 getInstace 등의 함수로 인스턴스를 가져다 쓸 수만 있도록 제한한다.

    devlog.joonas.io

    싱글톤 패턴

    싱글턴 패턴은 인스턴스를 하나만 생성하여 사용하는 패턴이다.

    개인적으로 가장 쉬우면서 흔하게 볼 수 있는 디자인 패턴이다.

    예시

    프로그램(또는 어플리케이션)의 설정 클래스가 있다고 치자. 그럼 이것은 프로그램 전체에서 마치 하나의 파일처럼 동작해야할것이다. 특히, 데이터베이스를 사용하는 클래스들이 주로 그렇다.

     

    어떤 게임에 아래와 같은 설정 클래스가 있다고 하자.

    public Config {
        private int volume = 10;
        private int theme = Colors.DARK;
    }

    그리고 이 설정을 다른 클래스(캐릭터, 키보드 관리자 등)에서도 사용하고 싶다.

    시스템 설정에서 설정한 볼륨만큼, 각 클래스에서 소리를 작게 재생해야하는 상황이라고 생각하면 된다.

    public Character {
        private Config config;
    }
    
    public Keyboard {
        private Config config;
    }

    두 클래스 Character와 Keyboard는 같은 설정을 가지고 있을까? 아니다.

    물론, call by reference로, 더 상위의 클래스로부터 하나의 config를 계속 이어받아서 가지고 있다면 같을 수 있지만 (대표적으로 Android의 Context), 서로 얽혀있는 종속성을 피하고 싶다. 그리고 실제로 종속될 필요도 없다.

    구현

    Config 클래스의 생성자를 직접 호출하지 못하도록 생성자를 private로 숨긴다.

    그리고 static으로 인스턴스가 한번만 생성되도록 하고, getInstance() 등의 함수로 하나의 인스턴스를 가져다 쓰게 강제하는 것이다.

    public Config {
        // static으로 메모리에 할당한다.
        private static Config instance;
        
        // 생성자를 숨겨서, 외부에서는 직접 생성할 수 없도록 한다.
        private Config() {
            // 실제 객체는 한번만 생성된다.
            instance = new Config();
        }
        
        // 외부에서는 가져다 쓰는 것만 가능하다.
        public static getInstance() {
            return instance;
        }
        
        /////// 아래는 실제 객체에 대한 methods, fields ///////
        
        // 실제 객체의 값들
        private int volume = 10;
        private double value = Math.random();
        
        public getValue() {
        	return value;
        }
    }
    
    // 이제 두 클래스는 같은 객체를 참조한다.
    public Character {
        private Config config = Config.getInstance();
    }
    
    public Keyboard {
        private Config config = Config.getInstance();
    }

    출력 결과

    main에서 인스턴스를 여러 개 만들어서 값을 확인해봐도 같은 것을 알 수 있다.

    Config obj1 = Config.getInstance();
    Config obj2 = Config.getInstance();
    System.out.println("obj1 " + obj1.getValue());
    System.out.println("obj2 " + obj2.getValue());
    
    // 출력:
    // obj1 0.4539129414226112
    // obj2 0.4539129414226112

    여러 가지 실험

    상속(Inheritance)

    그럼 싱글톤 클래스를 상속받으면 어떻게 될까? 예상하기에는 당연히 같은 인스턴스일 것이다.

    자식 클래스를 위해서 private가 아닌 protected로 바꾸고 실험을 해보았다.

    public class SingleToneObject {
      protected double value = Math.random();
      
      protected static SingleToneObject instance = new SingleToneObject();
    
      protected SingleToneObject() {
      }
      
      public static SingleToneObject getInstance() {
        return instance;
      }
    
      public double getValue() {
        return value;
      }
    
      public void newRandom() {
        value = Math.random();
      }
    }
    public class SingleChild extends SingleToneObject {
      @Override
      public double getValue() {
        return 100.0 + this.value;
      }
    }

    그리고 아래 순서로 코드를 실행해보았다.

    public class Main {
      public static void main(String[] args) {
        SingleToneObject obj1 = SingleToneObject.getInstance();
        SingleToneObject obj2 = SingleToneObject.getInstance();
        System.out.println("obj1 " + obj1.getValue());
        System.out.println("obj2 " + obj2.getValue());
        System.out.println("extends " + SingleChild.getInstance().getValue());
        obj1.newRandom(); // 내부 변수 변경
        System.out.println("obj1 " + obj1.getValue());
        System.out.println("obj2 " + obj2.getValue());
        System.out.println("extends " + SingleChild.getInstance().getValue());
      }
    }

    결과는

    obj1 0.8537930657276994
    obj2 0.8537930657276994
    extends 0.8537930657276994
    obj1 0.9935029581717479
    obj2 0.9935029581717479
    extends 0.9935029581717479

    모두 업데이트가 된 것 같지만, 이건 눈속임이다. Override한 getValue()가 전혀 동작하지 않은 것을 볼 수 있다.

    그럼 SingleToneObject obj2 = SingleChild.getInstance(); 를 하면 달라질까? 이미 내부에서 사용하는 getInstance()로 얻어온 instance는 SingleToneObject에 있는 것과 같다.

    이럴거면 상속을 굳이 받을 필요가 없다.

    포함(Composition)

    마찬가지로, 하나의 instance를 여러개로 들고 있어봐야 결국 같은 거다. main 함수의 obj1, obj2와 다를 것이 없다.

    문제점

    static 변수이므로 메모리에 계속 떠있다보니, 싱글톤으로 만든 인스턴스가 무거우면 문제가 된다.

     

    Comments