이번 주에는 실무에서 아마 가장 많이 사용하는 패턴 중 하나인 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 을 직접만들어 사용할 수 있게 되었습니다. 


하지만 주의할 것은 스터디에서도 살펴본 것 처럼 싱글톤 병에는 안 걸리게 조심합시다. ㅡㅡ^

반응형
Posted by N'