[ChAPTER 4] 실무 활용 패턴 (중) [Singleton 패턴] + 추가내용
이번 주에는 실무에서 아마 가장 많이 사용하는 패턴 중 하나인 Singleton 패턴을 다뤄보았습니다.
자료는 아래 포스팅에서 참고!
물론 웹 프로젝트에서는 직접 Singleton 을 만들기보다는 Bean 등록을 통해 Component 들을 사용하지만, 안드로이드 프로젝트 혹은 자바가 아닌 다른 환경에서는 직접 만들어야 할 일이 있을 수 있습니다.
그렇기 때문에 세 번째 실무 주제로 Singleton 을 선택하였습니다.
Singleton 은 클래스 내에서 한 개의 instance 만 생성하여, 그 인스턴스를 광역적으로 사용하기 위한 패턴입니다.
(웹 프로젝트에서는 각종 Component 를 Controller 가 광역적으로 사용하고 있습니다.)
1. Singleton 의 기본적인 컨셉
기본적인 Singleton 의 컨셉은 아래와 같이 정의해 볼 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * 싱글톤 예제 * * Created by Doohyun on 2017. 5. 14.. */ public class Singleton { // 인스턴스는 클래스 내부에서 공용으로 한개만 관리. private static Singleton instance = null; // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() {} // 인스턴의 접근 제한은 아래 메소드로만 가능. public static Singleton GetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } | cs |
instance 를 한 개만 생성하고 관리하는 것이 목적이므로, 클래스 내부에서 instance 를 공용으로 관리하고 instance 를 접근할 수 있는 정적 메소드를 제공해줍니다.
하지만 이 기본적인 컨셉은 안타깝게도 동시성 처리에 취약합니다.
내부 GetInstance 를 동시에 여러 Thread 가 접근할 시, instance 의 null 체크의 Thread-safe 를 보장할 수 없습니다.
2. Thread-safe 한 Singleton
Singleton 의 Thread-safe 문제를 해결하기 위해 생각해 볼 수 있는 방법은 공용으로 관리하려는 instance 에 대한 명시적 초기화를 해주는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * 명시적 초기화한 싱글톤 예제 * * Created by Doohyun on 2017. 5. 14.. */ public class Singleton { // 인스턴스의 명시적 초기화 private static Singleton instance = new Singleton(); // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() {} // 인스턴의 접근 제한은 아래 메소드로만 가능. public static Singleton GetInstance() { return instance; } } | cs |
클래스 로드 타임에 미리 instance 가 생성되기 때문에 Thread-safe 합니다.
이런 방식을 Eager-binding (부지런한 할당)이라 합니다. 하지만 Eager-binding 방식은 싱글톤의 instance 를 사용하지 않음에도 바로 초기화가 되어버릴 수 있습니다.
아래 예제 처럼 말이죠.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class Singleton { // 인스턴스의 명시적 초기화 private static Singleton instance = new Singleton(); // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() { System.out.println("Create Singleton!!"); } // 인스턴의 접근 제한은 아래 메소드로만 가능. public static Singleton GetInstance() { return instance; } /** * 싱글톤의 정적 메소드 */ public static void RunStaticTask() { } } Singleton.RunStaticTask(); // CONSOLE LOG // Create Singleton!! | cs |
하지만 만약 Singletone 을 만들기 위한 비용이 크고, 많은 Singleton 들이 Eager-binding을 시도한다면 부하가 클 수도 있습니다. 즉 Instance 를 사용할 때 생성하는 Lazy-binding(게으른 할당)이 필요해 보입니다.
Eager-binding 이 무조건 나쁜 것은 아닙니다. 서버 프로젝트의 경우 오히려 서버가 켜질 때 각 컴포넌트들이 미리 Instance 를 생성하고, Client 의 요청에 딜레이 없이 바로 응답해주는 것이 좋습니다.
3. Lazy-binding 방식의 Singleton 생성
다시 기본적인 컨셉으로 돌아와서 생각해 봤을 때, 직관적인 방법은 오직 한 개의 스레드 만 접근하도록 Lock 을 걸어주는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * 스레드의 Lock 을 이용한 싱글톤 예제 (1) * * Created by Doohyun on 2017. 5. 14.. */ public class Singleton { // 인스턴스의 명시적 초기화 private static Singleton instance = null; // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() { } // synchronized 로 Lock 을 건다. // 이 메소드는 오직 한 스레드 씩 접근 가능 public static synchronized Singleton GetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } | cs |
메소드에 Lock 을 걸었기 때문에 Thread-safe 함은 보장합니다.
하지만 매번 사용할 때마다 한 개의 Thread 만 접근 가능하다는 것은 조금 불편한 진실인 듯 합니다. ㅡㅡ^
그렇기 때문에 정말 Thread-safe 한 부분만 Lock 이 걸리도록 변경해보려 합니다. Thread-safe 함이 보장되야할 부분은 instance 의 생성 부분입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * 스레드의 Lock 을 이용한 싱글톤 예제 (2) * * Created by Doohyun on 2017. 5. 14.. */ public class Singleton { // 스레드 간 최산 값을 읽어주기 위해 volatile 을 이용한 가시성 변수 사용 private static volatile Singleton instance = null; // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() { } // 개선된 Lock 처리 public static Singleton GetInstance() { if (instance == null) { // instance 의 생성 부분만을 Lock 처리 synchronized (Singleton.class) { instance = new Singleton(); } } return instance; } } | cs |
하지만 이 방식은 가시성 변수를 사용하고 있습니다.
instance 변수는 모든 스레드가 최신 값을 읽게 하기 위해 CPU 의 캐시를 사용하지 않으며, 그렇기 때문에 무조건 메모리로부터 데이터를 불러오게 됩니다.
- 간략한 운영체제 속 내용 소개 ㅡㅡ^
데이터는 모두 메모리(Heap, Data, Stack 영역) 위에 저장되어 있고, 보통은 CPU가 계산을 하기 위해 데이터를 읽어온 뒤 캐싱 작업을 합니다.
캐싱작업을 하는 이유는 CPU 에서 미리 데이터를 저장함으로써, 보다 빠르게 읽고 계산하기 위해서죠.
하지만 여러 스레드가 변수를 동시접근을 하여 데이터를 조작하면, CPU 캐시의 값이 최신 값임을 보장할 수 없게 됩니다.
즉 변경된 값을 못 읽어올 수 있죠.
그렇기 때문에 변수에 volatile 키워드를 붙여 가시성 변수임을 선언하며, 그 변수는 항상 메모리에서 읽어오게 됩니다.
하지만 CPU 의 캐시에서 읽어오는 것보다 비용이 언제나 크겠죠? ㅜㅡㅜ
3. Lock 을 사용하지 않은 Lazy-bindig 방식의 Singleton
MARYLAND 대학의 연구원인 Bill puge 는 Lock 을 사용하는 방식은 단점을 해결하기 위해 새로운 Singleton 생성방식을 고안했습니다.
그 방식의 이름은 "Initialization on demand holder idiom" 으로 내부 클래스를 이용한 Holder 를 사용하는 기법은 클래스 로더 규칙에 의해 Lazy-binding 을 보장합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** * Initialization on demand holder idiom 방식의 싱글톤 생성 * * Created by Doohyun on 2017. 5. 14.. */ public class Singleton { // Instance 를 감싸는 홀더 생성. 클래스로더 규칙에 의해, ManagerHolder 가 로드될 때 Instance 생성 private static final class ManagerHolder { private static final Singleton Instance = new Singleton(); } // 외부에서 인스턴스를 생성 못하도록 접근제한 private Singleton() { } // 개선된 Lock 처리 public static Singleton GetInstance() { return ManagerHolder.Instance; } } | cs |
내부에 있는 ManagerHolder 는 Private 으로 접근제한 되어 있으며, 오직 GetInstance 를 통해서만 사용할 수 있습니다. 즉 GetInstance 가 호출 될 때 ManagerHolder 클래스가 로드되며 그 순간 명시적 초기화를 하고있는 내부의 Instance 가 생성됩니다.
4. Enum 을 사용하는 Singleton
Enum(열거형) 을 통해서도 Singleton 을 만들 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * ENUM 을 이용한 싱글톤 * * Created by Doohyun on 2017. 5. 14.. */ public enum Singleton { INTANCE; public void doSometing(){} } // Enum 싱글톤 사용법 Singleton.INTANCE.doSometing(); | cs |
간결한 사용법 때문에 최근 권장하는 스타일입니다.
이 방식은 열거형 특성 상, 컴파일 시점에 미리 Instance 가 생성되기 때문에 Thread-safe 함을 보장합니다. 하지만 무조건 Eager-binding 입니다.
Singleton 을 만드는 여러 방식을 알아보았습니다. 우리는 Spring 이 아니더라도 Singleton 을 직접만들어 사용할 수 있게 되었습니다.
하지만 주의할 것은 스터디에서도 살펴본 것 처럼 싱글톤 병에는 안 걸리게 조심합시다. ㅡㅡ^
'스터디 > [STUDY] OOP' 카테고리의 다른 글
[CHAPTER 5] 실무 활용 패턴 (하) (0) | 2017.05.18 |
---|---|
[CHAPTER 4] 실무 활용 패턴 (중) + 과제 (0) | 2017.05.14 |
[CHAPTER 3] 실무 활용 패턴 (상)[Template method 패턴] + 추가내용 (0) | 2017.05.13 |
[CHAPTER 4] 실무 활용 패턴 (중) (0) | 2017.05.08 |
[CHAPTER 3] 실무 활용 패턴 (상)[Strategy 패턴] + 추가내용 (4) | 2017.04.27 |