본문 바로가기

CS

[GoF] Singleton

Singleton패턴

GoF를 검색하면 가장 처음으로 나오는 객체 생성에 관련된 패턴이다. 해당 패턴의 특징은 다음과 같다.

  • 객체를 오직 1개만 생성하는 패턴
  • 생성자를 공개하지 않고 별도의 지정된 함수를 호출하는 방식으로 객체를 반환
  • 매우 큰 객체 1개를 여러곳에서 공유하며 사용할 때 유리한 패턴

소스코드(golang)

// 싱글톤 인스턴스
var instance *object

type object struct {
}
// 싱글톤 객체를 조회하는 인스턴스
func GetInstance() *object {
	// 초기화되지 않았으면 인스턴스를 초기화
	if instance == nil {
		instance = &object{}
	}
	return instance
}

func (receiver object) Execute() {
	receiver.printHello()
}

func (receiver object) printHello() {
	fmt.Println("hello")
}

func TestSingleTone(t *testing.T) {
	obj := GetInstance()
	obj.Execute()
}

멀티스레드와 싱글톤

위의 소스코드에는 문제점이 있다. 바로 멀티프로그래밍 상황에선 싱글톤이 깨질 수 있다는 것이다.

다음의 코드를 보자

var instance *object

type object struct {
	Value int
}

func GetInstance(value int) *object {
	if instance == nil {
		instance = &object{value}
	}
	return instance
}

func (receiver object) Execute() {
	receiver.printHello()
}

func (receiver object) printHello() {
	fmt.Println("hello", receiver.Value)
}

func TestSingleTone(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		obj := GetInstance(1)
		obj.Execute()
		wg.Done()
	}()
	go func() {
		obj := GetInstance(2)
		obj.Execute()
		wg.Done()
	}()
	wg.Wait()
}

서로다른 고루틴에서 같은 싱글톤 인스턴스를 최초로 접근한다. 위의 코드를 보면 hello 1 혹은 hello 2 를 2번 출력할 것이라고 생각할 것이다. 그러나 실제로 실행시켜보면 다음과 같다.

=== RUN   TestSingleTone
hello 2
hello 1
--- PASS: TestSingleTone (0.00s)

같은 인스턴스임에도 불구하고 서로 다른 출력결과가 나온다.

이러한 이유는 싱글톤 인스턴스가 초기화가 안되어있기 때문이다. 인스턴스 객체는 null이므로 새로운 객체를 생성해서 반환해줄 것이다. 이 과정이 스레드1이 호출할 때와 스레드2가 호출할 때 동시에 발생되므로 null이라고 판단하여 고루틴들이 호출한 인스턴스 호출에 대해 인스턴스 초기화 작업을 수행할 것이다. 즉 고루틴들은 같은 싱글톤 객체를 접근한다고 생각하였지만 실제론 서로 다른 곳에 저장되어 있는 싱글톤 객체를 접근하여 사용하고 있다. 싱글톤이 깨져버린 것이다.

싱글톤의 구현 난이도가 높은 이유는 위의 상황처럼 싱글톤이 깨지는 상황을 방지해야한다는 것이다. 개발이라는 것은 혼자하는 것이 아닌 여러사람이 협업을 수행한다(처음부터 끝까지 혼자하는 능력자빼고). 내가 싱글톤으로 라이브러리를 개발하였다는 것은 다른 사람은 소스코드를 까보거나 알려주지 않으면 모른다. 설령 알려준다 하더라도 까먹을 수도 있다. 다른 개발자가 내가 짠 라이브러리를 쓸려고 하는데 싱글톤인걸 모르는 상태에서 사용하다보면 위의 사례처럼 원치않는 동작이 발생할 수 있다. 심지어 동작할 때마다 서로 다른 결과가 나오니 디버깅조차 어렵다.

여러 스레드에서 싱글톤을 사용할 때 깨지지 않기 위해선 다음과 같은 방법으로 방지할 수 있다.

1. 컴파일 타임에서 초기화

멀티스레드에서 싱글톤이 깨지는 상황은 인스턴스가 초기화가 안되어있는 상황에서 동시에 호출을 수행하기 때문이다. 즉 컴파일타임에서 인스턴스를 생성한다면 싱글톤이 깨지지 않는다

var instance *object = &object{0}

type object struct {
	Value int
}

func GetInstance(value int) *object {
	return instance
}

func (receiver object) Execute() {
	receiver.printHello()
}

func (receiver object) printHello() {
	fmt.Println("hello", receiver.Value)
}

func TestSingleTone(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		obj := GetInstance(1)
		obj.Execute()
		wg.Done()
	}()
	go func() {
		obj := GetInstance(2)
		obj.Execute()
		wg.Done()
	}()
	wg.Wait()
}

실행결과는 다음과 같다.

=== RUN   TestSingleTone
hello 0
hello 0
--- PASS: TestSingleTone (0.00s)

컴파일 타임에서 초기화하고 런타임에선 컴파일 타임에서 생성한 객체만 반환되므로 싱글톤이 깨지지 않는다. 그러나 해당 방법은 메모리 낭비가 발생한다. 싱글톤은 매우 큰 객체 1개를 여러곳에서 사용할 때 유리하다. 즉 싱글톤은 매우 큰 객체를 다룰 때가 많다는 것이다. 컴파일 타임에서 싱글톤 인스턴스를 초기화 한다는 것은 실행할 때 인스턴스가 메모리에 적재된다는 것이다. 즉 싱글톤 객체를 사용하지 않을때도 메모리를 차지하게 되므로 메모리 낭비가 발생한다.

2. Mutex 제어

Mutex를 통해 인스턴스를 호출하는 함수는 순차적으로 호출되도록 제어하는 것이다.

var mutex = sync.Mutex{}

var instance *object

type object struct {
	Value int
}

func GetInstance(value int) *object {
	mutex.Lock()
	if instance == nil {
		instance = &object{value}
	}
	mutex.Unlock()
	return instance
}

func (receiver object) Execute() {
	receiver.printHello()
}

func (receiver object) printHello() {
	fmt.Println("hello", receiver.Value)
}

func TestSingleTone(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		obj := GetInstance(1)
		obj.Execute()
		wg.Done()
	}()
	go func() {
		obj := GetInstance(2)
		obj.Execute()
		wg.Done()
	}()
	wg.Wait()
}

실행결과를 보면 싱글톤이 깨지지 않음을 확인할 수 있다.

=== RUN   TestSingleTone
hello 2
hello 2
--- PASS: TestSingleTone (0.00s)

다만 Mutex는 개발자의 테크닉이 필요하다. 만약 Lock을 하고 Unlock을 하는 코드를 빠트리면 deadlock이 발생될 수 있다.

3. sync.Once 객체

싱글톤 객체에서 인스턴스 초기화는 최초 1회만 발생한다. 즉 최초1회만 어떻게든 넘기면 그 뒤엔 누가 인스턴스 생성을 호출하든 이미 생성된 인스턴스만 반환해주므로 싱글톤이 깨지지 않는다.

sync.Once객체는 특정 함수를 프로그램 동작중 최초 1회만 동작시킨다. 즉 이를 이용해 인스턴스가 호출될 때 최초 1회만 인스턴스를 초기화하도록 제어한다.

var once sync.Once

var instance *object

type object struct {
	Value int
}
// 오직 1번만 인스턴스가 초기화되도록 수행
func GetInstance(value int) *object {
	once.Do(func() {
		instance = &object{value}
	})
	return instance
}

func (receiver object) Execute() {
	receiver.printHello()
}

func (receiver object) printHello() {
	fmt.Println("hello", receiver.Value)
}

func TestSingleTone(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		obj := GetInstance(1)
		obj.Execute()
		wg.Done()
	}()
	go func() {
		obj := GetInstance(2)
		obj.Execute()
		wg.Done()
	}()
	wg.Wait()
}

once가 인스턴스 초기화을 1번만 수행하도록 제어하므로 싱글톤이 깨지지 않는 것을 확인할 수 있다.

=== RUN   TestSingleTone
hello 2
hello 2
--- PASS: TestSingleTone (0.00s)

'CS' 카테고리의 다른 글

[자료구조] 해시맵  (0) 2022.07.13
[자료구조]BST, AVL트리  (0) 2022.07.06
[자료구조] 트리 개론  (0) 2022.07.04
[Web]CORS  (1) 2022.06.20
[GoF] 2. Factory  (0) 2022.03.22