본문 바로가기
golang/design pattern

golang design pattern #4 Singleton

by PudgeKim 2021. 11. 11.

Singleton 패턴은 특정 객체가 프로그램 실행중에 딱 한번만 생성되고 모두 같은 객체를 사용할 때 쓰입니다.

대표적으로 데이터베이스 관련 객체를 Singleton으로 많이 만듭니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
type singletonDB struct {
   info map[string]int
}
 
var once sync.Once
var db *singletonDB
 
func GetDB() *singletonDB {
    once.Do(func() {
        // db 초기화 작업
    })
    return db
}
cs

 

sync.Once를 활용하면 한번만 실행되는 코드를 만들 수 있습니다.

즉, GetDB함수를 호출하면 최초 호출시에만 once.Do 함수의 인자로 들어간 함수가 실행이 될테고
이 함수는 db 변수에 데이터베이스 관련 객체를 저장할 것입니다.

그 이후에는 GetDB함수를 호출하면 db는 nil 값이 아닐테고 데이터베이스 정보가 들어있는 db 변수만 return 하게 됩니다.

 

이러한 Singleton 패턴은 단점이 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
var db *singletonDB
 
func (sb *singletonDB) GetValue(name string) int {
    return sb.info[name]
}
 
func GetTotalValue(names... string) int {
    tot := 0
    for _, name := range names {
        tot += GetDB().GetValue(name)
    }
    return tot
}
cs

위와 같은 코드가 추가되었다고 가정해봅시다.

그렇다면 아래와 같은 테스트 코드를 작성해 볼 수 있습니다.

1
2
3
tot := GetTotalValue("kim", "alex")
ok := tot == (1 + 2)
fmt.Println(ok)
cs

(kim과 alex의 value가 1과 2라고 가정합니다.)

 

이러한 코드의 문제점은

1. 유닛테스트를 하고싶어도 우선 DB에 접속하는게 잘 이루어져야한다는 가정이 있어야합니다.
또한, DB의 실제 데이터에 의존하게 됩니다. 즉, 통합테스트가 되어버립니다.

2. 현재 GetTotal함수는 GetDB에 의존하므로 SOLID의 원칙중 하나인 DIP를 위반하게 됩니다.
(interface를 통한 추상화에 의존하게 만들어야 합니다.)

 

이런 문제를 해결하기 위한 코드는 다음과 같습니다.

1
2
3
type DB interface {
    GetValue(name string) int
}
cs

이렇게 interface를 하나 추가하면 GetTotalValue 함수를 다음과 같이 바꿀 수 있습니다.

1
2
3
4
5
6
7
func GetTotalValue(db DB, names... string) int {
    tot := 0
    for _, name := range names {
        tot += db.GetValue(name)
    }
    return tot
}
cs

interface 타입을 이용해 훨씬 더 유연하게 변경하였습니다.

 

테스트코드는 아래와 같이 변경할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
type DummyDB struct {
    dummyData map[string]int
}
 
func (d *DummyDB) GetValue(name string) int {
    if len(d.dummyData) == 0 {
        d.dummyData = map[string]int {
            "kim": 1,
            "alex": 2,
        }
    }
    return d.dummyData[name]
}
cs

이렇게 통합테스트가 아닌 유닛테스트를 위한 DummyDB 구조체를 만들고
위에서 정의한 interface에 따르기 위해 GetValue 메서드를 추가해줍니다.

1
2
tot := GetTotalValue(&DummyDB{}, "alex", "kim")
fmt.Println(tot == 3)
cs

interface에 의해 DummyDB도 인자로 들어갈 수 있으므로 위와 같은 테스트코드를 작성할 수 있습니다.

'golang > design pattern' 카테고리의 다른 글

golang design pattern #6 Bridge  (0) 2021.11.12
golang design pattern #5 Adapter  (0) 2021.11.12
golang design pattern #3 Prototype  (0) 2021.11.11
golang design pattern #2 Factory  (0) 2021.11.10
golang design pattern #1 Builder  (0) 2021.11.10