본문 바로가기
디자인패턴

[디자인패턴] 싱글톤 패턴 정리

by 미소여우 2022. 3. 23.
728x90

백기선님의 디자인 패턴 강의를 듣고 정리하는 글

목차는 아래를 참고해주세요.

 

[디자인패턴] 목차

싱글톤 패턴

somefood.tistory.com


싱글톤 패턴이란

인스턴스를 오직 한개만 제공하는 클래스를 의미한다. 시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 발생하는 경우가 생길 수 있다. 가령, 인텔리제이의 환경설정창을 여러 개 띄어두고 각각 설정을 다르게 저장하면, 문제가 발생함은 당연지사이다. 그렇기에 인스턴스를 하나만 제공하는 패턴이 필요해졌고, 이것이 싱글톤 패턴이다.

스프링의 빈들도 싱글톤 스코프로 구성되어있다.

싱글톤 구현 방법

방법1 - naive한 싱글톤

싱글톤 패턴을 쓰려면 new() 생성자를 막아줘야 한다. 그래서 생성자의 접근지시자를 private으로 지정해준다.

public class Settings {
	
    private static Settings instance;
    
    private Settings() {}
    
    public static Setting getInstance() {
    	if (instance == null) {
        	instance = new Settings(); 
        }
        
        return instance;
    }
}

이렇게 하면 외부에서는 new Settings()로 접근을 못하게 되고, 오직 Settings.getInsatnce()라는 스태틱 메서드를 호출해서 얻을 수 있다. 스태틱 메서드 내부를 보면 instance가 null인 경우에 new()로 하나 만들어 주고 이후부턴 같은 인스턴스를 반한해준다. 이게 가장 간단한 싱글톤 패턴 구조이다. 근데 이렇게 되면 멀티쓰레도 환경에서는 사용하기 힘들다.

만약 2개의 쓰레드가 있는데, 타이밍이 안 맞으면 둘 다 new Settings()가 실행되기 때문이다. 그러면 서로 다른 인스턴스를 받게 되는 것이다.

방법2 - synchronized 키워드로 멀티 쓰레드 환경 제어

스태틱 메서드에 synchronized를 적용하여 하나의 쓰레드만 들어오게 하여 동시에 들어오는 것을 막는다.

public class Settings {
	
    private static Settings instance;
    
    private Settings() {}
    
    public static synchronized Setting getInstance() {
    	if (instance == null) {
        	instance = new Settings(); 
        }
        
        return instance;
    }
}

하지만 동기화 처리 작업을 거치게 때문에 성능 저하를 초래할 수 있다.

방법3 - 이른 초기화(eager initialization) 사용

인스턴스 생성 비용이 비싸지 않다면 미리 선언해서 사용해도 된다. 그러면 어느 쓰레드든 같은 인스턴스를 반환받으니 쓰레드 세이프하게 사용할 수 있다.

public class Settings {
	
    private static final Settings INSTANCE = new Settings();
    
    private Settings() {}
    
    public static Setting getInstance() {
    	return INSTANCE;
    }
}

방법4 - double checked loking 사용

public class Settings {

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

instance 유무를 두 번 체크하는 것인데, synchronized (Settings.class) 부분에서 한 쓰레드만 들어올 수 있으니 이후 쓰레드는 대기하게 된다. 첫 번째 쓰레드를 통해 인스턴스가 만들어 졌을 테니 이후 쓰레드들은 기 생성된 인스턴스를 가져다 쓰면 된다.

추가적으로 volatile 키워드를 명시해주어야 한다. 자바 1.5부터 실행

왜 double checked가 기존 메서드에 synchronized때보다 효율적일까? 이유는 메서드에 걸려있으면 매 메서드 호출마다 동기화 작업이 필요하지만, 위와 같은 방식으로 하면 첫 번째 if문에서 instance가 존재할 시, 바로 끝나기에 동기화를 할 필요가 없어지기 때문이다.

방법5 - static inner 클래스 사용 (권장 방법 중 하나)

public class Settings {

    private Settings() {}

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

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

내부적으로 inner class를 생성하여 getInsatnce()가 호출 될 때 SettingsHolder가 로딩 되고 Settings() 인스턴스를 만들어 주기에 Lazy하게도 만들 수 있다. 또한 쓰레드 세이프하다.

싱글톤 패턴 구현 깨트리기

우리가 위에서 구현한 방법들은 특정 자바 구문을 사용하면 깨트릴 수 있다.

방법1 - 리플렉션

Constructor<Setting> constructor = Settings.class.getDeclaredConstrucotr();
constructor.setAccessible(true);
Settings settings1 = constructor.newInstance()

리플렉션을 사용하면 해당 클래스의 생성자에도 접근할 수 있고, 심지어 private 생성자인데도 접근이 가능하다. 그렇게 되면 새로운 인스턴스를 만들어 싱글톤이 깨지게 된다.

리플렉션을 모른다면 간단히 포스팅한 글이 있는데 여기를 참고해주시기 바란다.

방법2 - 직렬화 & 역직렬화 사용하기

직렬화는 자바의 내용을 바이트 코드등으로 변환해 디스크나 파일에 저장할 수 있는 형태로 바꾸는 것을 말하고, 역직렬화는 직렬화 된 내용을 다시 자바가 이해할 수 있는 형태로 변환해준다고 이해하면 된다.

// 직렬화
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
    out.writeObject(settings);
}

// 역직렬화
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
    settings1 = (Settings5) in.readObject();
}

역직렬화를 하게 되면 생성자를 사용해서 새 인스턴스를 만들어 주기에 싱글톤이 깨지게 된다. 역직렬화를 하면서 내부적으로 readResolve()라는 메서드가 호출되면서 그렇다고 한다. 역직렬화는 막을 수 있지만 리플렉션은 어쩔 수 없다.

위에서 배운대로라면 강가에 내놓은 자식마냥 불안해서 못 쓸거 같은 이 싱글톤..  안전한 싱글톤은 대체 무엇일까..

안전하고 단순하게 구현하는 방법

enum 사용하기

public enum Settings {

    INSTANCE;

}

enum으로 선언하면 리플렉션으로 인스턴스를 만들 수 없다. 단점으론 로딩하는 순간 만들어 진다는 것과 상속을 받지 못한다.

enum은 기본적으로 Enum이란 클래스를 상속받았고, 직렬화 역질렬화를 할 수 있다.

싱글톤 패턴이 쓰이는 곳

싱글톤 패턴이 실무에서 쓰이는 곳은 다음과 같다.

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

 

728x90

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

[디자인패턴] 팩토리 메소드 패턴  (0) 2022.03.27
[디자인패턴] 목차  (0) 2022.03.23

댓글