본문 바로가기
GoF의 디자인 패턴

싱글톤(Singleton) 패턴

by 아토로 2021. 12. 29.

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

시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개 일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요하다.

 

구현 방법 1

private 생성자에 static 메서드

public class Settings {
    private static Settings instance;

    private Settings() { }

    public static Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }
        return instance;
    }
}
  1. 생성자를 private으로 만든 이유
    • 외부에서 생성자 사용을 못하게 하여 new를 사용한 인스턴스화를 막기 위한 목적이다.
  2. getInstance() 메서드를 static으로 선언한 이유
    • 인스턴스 생성 없이 getInstance() 호출을 가능하게 하기 위한 목적이다.
  3. getInstance()가 멀티 쓰레드 환경에서 안전하지 않은 이유
    • instance가 아직 인스턴스화 되지 않은 상황에서 첫 번째 쓰레드가 if문 안으로 들어왔으나 아직 new를 실행하지 않은 상태일 때 다른 쓰레드가 if문 안으로 들어온다면 서로 다른 인스턴스가 생성되게 된다.

구현 방법 2

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

public static synchronized Settings getInstance() {
    if (instance == null) {
        instance = new Settings();
    }
    return instance;
}
  1. 자바의 동기화 블럭 처리 방법은?
    • 메서드 앞에 synchronized를 붙이거나, 메서드 내의 코드 일부를 블럭으로 감싸고 블럭 앞에 synchronized을 붙인다.
    • 블럭 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납한다.
    • 해당 객체의 lock을 가지고 있는 쓰ㄷ레드만 임계 영역의 코드를 수행할 수 있다.
  2. getInstance() 메소드 동기화시 사용하는 락(lock)은 인스턴스의 락인가 클래스의 락인가? 그 이유는?
    • static 메서드에 동기화 블럭을 설정하면 클래스 단위로 락이 걸린다.

구현 방법 3

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

private static final Settings INSTANCE = new Settings();

private Settings() {}

public static Settings getInstance() {
    return INSTANCE;
}
  1. 이른 초기화가 단점이 될 수도 있는 이유?
    • 클래스가 메모리에 할당되는 시점에 생성되므로 생성되는데 리소스가 많이 필요하거나 변수를 사용하지 않는 경우 불필요한 리소스 소모가 발생한다.
  2. 만약에 생성자에서 checked 예외를 던진다면 이 코드를 어떻게 변경해야 할까요?
    • try-catch를 사용하여 unchecked 예외로 변환시켜야 한다.

구현 방법 4

double checked locking으로 효율적인 동기화 블럭 만들기
- volatile 키워드를 사용해야하고, java 1.5 이상에서만 동작함

public static Settings getInstance() {
    if (instance == null) {
        synchronized (Settings.class) {
            if (instance == null) {
                instance = new Settings();
            }
        }
    }
    return instance;
}
  1. double check locking이라고 부르는 이유?
    • 인스턴스의 생성 여부를 체크 하고, synchronized 블럭 안에서 한번 더 체크하기 때문이다.
    • 첫 번째 체크를 통과한 쓰레드들만 동기화 블럭에 적용된다.
  2. instacne 변수는 어떻게 정의해야 하는가? 그 이유는?
    • volatile 키워드를 사용해야 한다.
    • 인스턴스를 메인 메모리에 저장하고 읽기 때문에 값 불일치 문제를 해결할 수 있다.

구현 방법 5

static inner 클래스를 사용하는 방법
- 멀티쓰레드에 안전한 방법, 지연 로딩 가능, 구현이 복잡하지 않음

private Settings() {}

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

public static Settings getInstance() {
    return SettingsHolder.INSTANCE;
}
  1. 이 방법은 static final를 썼는데도 왜 지연 초기화(lazy initialization)라고 볼 수 있는가?
    • inner class는 클래스가 처음 사용될 때 초기화가 수행된다.
    • getInstance()를 호출하는 시점에 inner class인 SettingsHolder에 접근하게 되고, 이때 초기화가 수행되기 때문에 지연 초기화처럼 수행되게 된다.

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

리플렉션을 사용한다면?

Settings settings = Settings.getInstance();

Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Settings settings1 = declaredConstructor.newInstance();

System.out.println(settings == settings1);
  1. 리플렉션에 대해 설명하세요.
    • 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API
  2. setAccessible(true)를 사용하는 이유는?
    • private 생성자에 접근하기 위한 목적이다.

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

직렬화 & 역직렬화를 사용한다면?

Settings settings = Settings.getInstance();
Settings settings1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
    out.writeObject(settings);
}

try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
    settings1 = (Settings) in.readObject();
}

System.out.println(settings == settings1);
  1. 자바의 직렬화 & 역직렬화에 대해 설명하세요.
    • 직렬화: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 바이트(byte) 형태로 변환
    • 역직렬화: 바이트로 변환된 데이터를 다시 객체로 변환
  2. SerializableId란 무엇이며 왜 쓰는가?
    • 직렬화된 클래스의 버전을 기억하여 로드된 클래스와 직렬화된 객체가 호환되는지 확인한다.
    • SerializableId가 다르면 역직렬화 할 수 없다.
  3. try-resource 블럭에 대해 설명하세요.
    1. try 코드 블럭이 끝나면 자동으로 자원을 종료해주기 때문에 명시적으로 자원 반환을 하지 않아도 된다.

직렬화 & 역직렬화는 방지 가능하다.

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

구현 방법 6

enum을 사용하는 방법
- 리플랙션에 안전한 유일한 방법

public enum Settings {
    INSTANCE;
}
  1. enum 타입의 인스턴스를 리플랙션을 만들 수 있는가?
    • enum 타입은 리플랙션을 할 수 없도록 막혀있어서 리플랙션에 안전하다.
  2. enum으로 싱글톤 타입을 구현할 때의 단점은?
    • 클래스가 메모리에 할당되는 시점에 인스턴스가 미리 만들어진다. 초기화 시점이 문제가 되지 않다면 가장 안전한 방법이다.
    • enum은 상속이 불가능하다.
  3. 직렬화 & 역직렬화 시에 별도로 구현해야 하는 메서드가 있는가?
    • enum 타입은 enum 클래스를 상속받게 되는데, enum 클래스는 Serializable을 이미 구현하고 있기 때문에 추가적인 구현이 필요 없다.

실무 적용 사례

  • 스프링에서 빈의 스코프 중에 하나로 싱글톤 스코프
  • 자바 java.lang.Runtime
  • 다른 디자인 패턴(빌더, 퍼사드, 추상 팩토리 등) 구현체의 일부로 쓰이기도 한다.

예제 코드

https://github.com/jsyang-dev/study-designpattern/tree/master/src/main/java/me/study/designpattern/singleton

'GoF의 디자인 패턴' 카테고리의 다른 글

어댑터(Adapter) 패턴  (0) 2022.01.13
프로토타입(Prototype) 패턴  (0) 2022.01.10
빌더(Builder) 패턴  (0) 2022.01.08
추상 팩토리 패턴  (0) 2022.01.06
팩토리 메소드(Factory method) 패턴  (0) 2022.01.04

댓글