Singleton 패턴
하나의 클래스에 대해 하나의 인스턴스만 존재하는 패턴 go에서는 sync.Once를 통해 구현 가능하다.
package main
import (
"fmt"
"sync"
)
type User struct {
uid int
name string
level int
}
type userDatabase struct {
users map[int]*User
}
var once sync.Once
var instance *userDatabase
func (d *userDatabase) GetUser(uid int) (*User, bool) {
user, ok := d.users[uid]
return user, ok
}
func GetUserDatabase() *userDatabase {
// sync.Once.Do()는 딱 한번만 실행
once.Do(func() {
db := &userDatabase{
map[int]*User{
1: {1, "zz", 2},
2: {2, "지존도적z", 32},
3: {3, "", 21},
4: {4, "zz", 27},
},
}
instance = db
})
return instance
}
func main() {
db := GetUserDatabase()
user2, _ := db.GetUser(2)
fmt.Println(user2)
}
실행결과
&{2 지존도적z 32}
singleton에서 테스트 코드 작성시 문제점
전체 유저의 레벨의 합을 구하는 함수가 필요하다고 해보자.
func GetUserTotalLevel(uids []int) int {
total := 0
for _, uid := range uids {
total += GetUserDatabase().users[uid].level
}
return total
}
실제 라이브 db는 수치가 변경될 수 있기때문에 dummy db를 만들어서 테스트를 수행해야 한다. 하지만 위 코드에서
total += GetUserDatabase().users[uid].level
부분은 userDatabase에 종속적이므로 테스트를 작성하기 쉽지 않다. (종속성 반전 원칙에 위배됨)
해결방법
인터페이스를 통해 단계를 추상화하여 종속성 반전을 통해 해결한다.
// interface
type Database interface {
GetUser(int) (*User, bool)
}
type DummyDatabase struct {
users map[int]*User
}
func (d *DummyDatabase) GetUser(uid int) (*User, bool) {
if len(d.users) == 0 {
d.users = map[int]*User{
1: {1, "zz", 2},
2: {2, "지존도적z", 32},
3: {3, "", 21},
4: {4, "zz", 27},
}
}
user, ok := d.users[uid]
return user, ok
}
// GetUserTotalLevel() 함수 수정
func GetUserTotalLevel(db Database, uids []int) int {
total := 0
for _, uid := range uids {
user, _ := db.GetUser(uid)
total += user.level
}
return total
}
func main() {
liveDb := GetUserDatabase()
liveTotalLevel := GetUserTotalLevel(liveDb, []int{1, 3, 4})
fmt.Println(liveTotalLevel == 50)
// test code
db := &DummyDatabase{}
totalLevel := GetUserTotalLevel(db, []int{1, 3, 4})
fmt.Println(totalLevel == 50)
}
실행결과
true
true