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