디자인패턴

싱글톤 패턴(singleton pattern)

JUNGKEUNG 2022. 6. 4. 20:05
반응형

싱글톤 패턴(Singleton Pattern) 이란?

  • 인스턴스를 오직 하나만 제공하는 패턴을 싱글톤 패턴이라고 한다.
  • 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다.
  • 생성자가 여러차례 호출되어도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 리턴한다. 클래스의 인스턴스가 오직 1개만 생성되는 것을 보장하는 디자인 패턴이다.

 

 

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
  • 의존관계상 클라이언트가 구체 클래스에 의존한다 -> DIP를 위반한다
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다
  • 테스트하기 어렵다
  • 내부 속성을 변경하거나 초기화 하기 어렵다
  • private 생성자로 자식 클래스를 만들기 어렵다
  • 결론적으로 유연성이 떨어진다
  • 안티패턴으로 불리기도 한다

 

 

간단한 싱글톤 구현

package me.likelen.study.singleton;

public class Settings1 {

    private static Settings1 instance;

    private Settings1() {
    }

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

1. 생성자를 private으로 만든 이유?

  • 오직 한 개의 인스턴스에만 접근하기 위해 생성자의 노출을 막기 위해서 이다.

 

2. getInstance() 메서드를 static으로 선언한 이유?

  • 외부에서 생성자를 호출 못하도록 private으로 막아 놓았기 때문에 외부에서는 인스턴스를 만들지 못한다. 따라서 해당 메서드를 호출하려면 전역으로 설정해두어야 한다.

 

3. getInstance()가 멀티쓰레드 환경에서 안전하지 않은 이유?

  • 쓰레드가 동시에 인스턴스를 생성하면 값이 달라질 수 있기 때문에 안전하지 않다.

 

 

동기화(synchronized)를 사용해 멀티쓰레드 환경에 안전하게 만드는 방법

package me.likelen.study.singleton;

public class Settings2 {

    private static Settings2 instance;

    private Settings2() {
    }

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

        return instance;
    }
}

1. 자바의 동기화 블럭 처리 방법은?

  • synchronized키워드를 사용하여 여러 쓰레드가 동시에 접근하지 못하도록 막는다.
  • 단점은 Synchronized 키워드 사용하면, 해당 메서드가 호출될 때마다 동기화 처리 작업을 수행해야 하는데 이는 성능에 불이익이 있을 수 있다.
  • 미리 만들어놓은 인스턴스를 호출해서 사용하기만 하면 되는데 단점은 사용하지 않으면 애플리케이션이 실행될 때마다 많은 리소스를 차지하게 된다.

2. getInstance() 메소드 동기화 시 사용하는락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?

  • 클래스 락이다. 만약 락이 인스턴스의 락이면, 동기화시 하나의 객체를 보장할 수 없게 되기 때문이다.

 

 

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

package me.likelen.study.singleton;

public class Settings3_2 {

    private static final Settings3_2 INSTANCE = new Settings3_2();

    private Settings3_2() {
    }

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

1. 이른 초기화가 단점이 될 수도 있는 이유?

  • 생성자에 많은 리소스를 사용되는 경우에는 좋지 않다.

2. 만약 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?

  • 기본적으로 함수에서 checked exception을 던지면 함수를 호출하는 쪽에서 try-catch문으로 감싸야한다. 하지만 예제처럼 변수를 초기화하는 과정에서는 try-catch문을 사용할 수 없다. 그럴 경우 static {} 블록을 이용해서 instance를 초기화하면 되는데 이경우 final 키워드를 사용할 수 없다.

 

 

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

package me.likelen.study.singleton;

public class Settings3 {

    private static volatile Settings3 instance;

    private Settings3() {
    }

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

1. double check locking이라고 부르는 이유?

  • ckecking을 두 번 했기 때문. 두 개의 쓰레드가 동시에 if문을 통과했다 하더라도 synchronized때문에 하나의 쓰레드만 안으로 들어갈 수 있다. 그리고 인스턴스를 생성하고 빠져나가는데 이때 다른 if문을 통과한 쓰레드가 synchronized로 들어오더라도 인스턴스가 이미 생성돼있기 때문에 if문에서 튕겨나간다.

2. instance 변수는 어떻게 정의해야 하는가? 그 이유는?

  • 변수에 volatile 키워드를 붙여야 한다. volatile을 사용하지 않은 변수는 성능 향상을 위해 CPU 캐시에 저장한다. 이 경우 쓰레드가 변숫값을 읽어올 때 각각의 CPU의 캐시에서 가져오기 때문에 값이 달라 값의 불일치가 발생한다.

 

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

package me.likelen.study.singleton;

public class Settings4 {

    private static volatile Settings4 instance;

    private Settings4() {
    }

    private static class Settings4Holder {
        private static final Settings4 INSTANCE = new Settings4();
    }

    private static Settings4 getInstance() {
        return Settings4Holder.INSTANCE;
    }
}

1. 리플렉션에 대해 설명하세요 

리플렉션은 구체적인 클래스의 타입을 몰라도 안에 선언되어있는 함수, 변수들에 접근할 수 있게 해주는 자바의 api

 

2. setAccessible(true)를 사용하는 이유는?

기본 생성자는 이 예제에서 private로 선언되어 있다. 즉 외부에서는 호출할 수 없다는 것인데, setAccessible(true)를 통해 Constructor <Settings> 타입으로 받은 declaredConstructor, 기본 생성자를 사용 가능하게 해 newInstance()를 사용해 새로운 객체를 만들 수 있게 하기 때문이다

 

 

 

싱글톤 패턴 구현 깨트리는 방법 1

[방법 1] 리플렉션

public class Main {
  public static void main(String[] args)
      throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Settings settings = Settings.getInstance();

    // [방법1]
    // 리플렉션을 사용하여 new 연산자를 이용한것과 같이 인스터스를 생성했다.
    Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Settings settings2 = constructor.newInstance();

	// singleton 이 깨진것을 확인 할 수 있다.
    System.out.println(settings.equals(settings2)); // fase
    System.out.println(settings == settings2); // fase
  }
}

1. 리플렉션에 대해 설명하세요 

리플렉션은 구체적인 클래스의 타입을 몰라도 안에 선언되어있는 함수, 변수들에 접근할 수 있게 해주는 자바의 api

 

 

2. setAccessible(true)를 사용하는 이유는?

기본 생성자는 이 예제에서 private로 선언되어 있다. 즉 외부에서는 호출할 수 없다는 것인데, setAccessible(true)를 통해 Constructor<Settings>타입으로 받은 declaredConstructor, 기본생성자를 사용가능하게해 newInstance()를 사용해 새로운 객체를 만들 수 있게하기 때문이다

 

 

싱글톤 패턴 구현 깨트리는 방법 2

[방법 2] 직렬화 역직렬화

public class Main {
  public static void main(String[] args) throws IOException, ClassNotFoundException {

    // [방법2]
    // 직렬화 & 역직렬화
    // 자바는 오브젝트를 파일 형태로 디스크에 저장해뒀다가(직렬화) 다시 읽어 드릴수 있다.(역직렬화)
    // 파일로 저장해뒀다가 로딩할 수 있다는 뜻이다.

    Settings settings = Settings.getInstance();
    Settings settings2 = null;

    // 1) 직렬화
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
      out.writeObject(settings); // 여기서 객체가 파일에 써진다.
    }

    // 2) 역직렬화
    try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
      settings2 = (Settings) in.readObject();
    }
    // 역직렬화를 하게되면 생성자를 사용해서 새로 인스턴스를 만들어준다.
    System.out.println(settings == settings2); // false

    // 하지만 대응 방안이 있다. 
    System.out.println(settings == settings2); // true
  }
}

// 역직렬화 대응 방안.
// 역직렬화 시 해당 메서드가 자동으로 호출되는데, 원래는 Settings 인스턴스를 내려주게된다.
// 하지만 우리는 임의로 원래의 인스턴스가 내려가도록 설정해두어 singleton 을 지킬 수 있다.
public class Settings implements Serializable {
  public Settings() {}

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

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

  protected Object readResolve() {
    return getInstance();
  }
}

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

  • 직렬화는 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술이며 역직렬화는 byte로 변환된 Data를 원래대로 Object나 Data로 변환하는 기술이다

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

  • Serializable를 상속받는 경우 클래스의 버전 관리를 위해 serialVersionUID를 사용한다. 이 serialVersionUID변수를 명시적으로 선언해 주지 않으면 컴파일러가 계산한 값을 부여하는데 Serializable Class 또는 Outer Class에 변경이 있으면 serialVersionUID값이 바뀌게 된다. 만약 Serialize 할 때와 Deserialize 할 때의 serialVersionUID 값이 다르면 InvalidClassExcepions가 발생하여 저장된 값을 객체로 Restore 할 수 없다

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

  • try-resource 블럭은 기존의 try-catch-final 블럭에서 사용하고 꼭 종료해줘야 하는 resource를 사용할 때 final 블럭에서 resource를 해제하는데, try-resource 블럭을 사용하면 따로 명시적으로 resource를 해제해주지 않아도 자동으로 해제해 준다

 

 

리플렉션과 직렬화/역직렬화를 이용하여 싱글톤을 깨트리는 것까지 막고 싶은 경우엔 Enum을 사용하자.

  • enum으로 singleton 구현 시 리플렉션(Reflection)에 안전한 코드가 된다.
  • enum 은 리플렉션에서 new 인스턴스를 사용할 수 없도록 막아놨기 때문이다. 
  • enum에서도 생성자, 프로퍼티, 메서드 모두 구현이 가능하다.
  • 직렬화 / 역직렬화를 통해 인스턴스 생성 시에도 안정적이다.
    • enum 은 기본적으로 Serialize를 구현하고 있는데 이때 같은 인스턴스가 반환되도록 설정되어있다.
  • 단점은 클래스를 로딩하는 순간 미리 만들어진다는 점과 상속을 사용하지 못한다. 
  • 그것이 크게 문제가 되지 않는다면, 가장 완벽한 방법일 수 있다.

 

싱글톤 패턴 구현 방법

public enum Settings { 
 INSTANCE; 
}

1. enum 타입의 인스턴스를 리플렉션을 통해 만들 수 있는가?

  • 만들 수 없다. enum 타입의 클래스는 리플렉션을 통해 만들 수 없도록 제한한다.

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

  • 단점은 이른 초기화와 같이 미리 만들어진다는 점이다. 그리고 상속을 할 수 없다.

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

  • 별다른 장치가 없어도 Enum 클래스는 직렬화&역직렬 화가 된다. 그러나, getResolves() 구현 시 역직렬화시 변경을 가할 수 있다.

 

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

  • 생성자를 private으로 설정한 다음, 인스턴스를 생성할 메서드를 하나 구현한다.
    이때 인스턴스를 생성해주는 메서드는 public static으로 설정하여 외부에서 전역 메서드로 호출될 수 있게끔 설정한다.
  • 위 방법과 동일하게 생성하지만 synchronized 키워드 설정하여 멀티쓰레드를 막는 방법
  • 이른 초기화 방법 (이는 static 한 필드들이 초기화되는 시점에 인스턴스가 생성되도록 하는 방법이다. 멀티쓰레드를 막는 방법)
  • double checked locking 방법 (인스턴스가 없는 경우에만 synchronized 가 걸린다.)
    syncronized block 을 설정하여 해당 block 전/후로 인스턴스 존재 여부를 확인한 후 인스턴스가 생성되도록 한다.
  • static inner 클래스를 사용하는 방법 (권장)

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

  • 멀티쓰레드 환경에서 안전하지 않을 수 있다.
  • 리플렉션과 직렬화&역직렬화를 이용하여 새로운 인스턴스 생성이 가능해진다.

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

  • 리플렉션과 직렬화&역직렬화를 이용했을 때 새로운 인스턴스 생성을 막을 수 있다. (장점)
  • 즉시 로딩(eager loading) 이 되어 인스턴스가 미리 만들어진다. (단점)
  • 상속을 사용하지 못한다. (단점)

7. static inner 클래스를 사용해 싱글톤 구현

package singleton;

public class Settings {
  public Settings() {}

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

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

 

 

 

싱글톤 패턴, 실무에선 어떻게 쓰일까?


  • 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
  • 자바 java.lang.Runtime
Runtime runtime = Runtime.getRuntime();
System.out.println(runtime.maxMemory());
System.out.println(runtime.freeMemory() );
  • 다른 디자인 패턴 구현체의 일부로 쓰이기도 한다.
    • Builder Parttern
    • Facade Parttern
    • Abstract Factory Parttern