본문 바로가기
Study/Design Pattern

[Design Pattern] 싱글톤 패턴(Singleton Pattern) with Golang

by _royJang 2021. 8. 26.

싱글톤 패턴이란?

싱글톤 패턴(Singleton Pattern)은 GoF가 정의한 23가지 디자인 패턴 중 생성 패턴에 속한다. 불필요한 객체의 생성을 없애고 단 한 번의 객체 생성으로 전역에서 효율적으로 객체를 사용할 수 있게 돕는 유용한 패턴이라 할 수 있다.

장점

  • 한번의 객체 생성을 통해 그 객체를 재활용하기 때문에 불필요한 메모리를 사용하지 않는다.
  • 처음 객체의 초기화 이후에는 따로 객체를 생성하지 않기에 읽어 들이는 시간이 크지 않다.
  • 전역인 하나의 인스턴스를 사용함으로 써 객체의 데이터를 공유하기가 간편하다.

단점

  • 멀티 쓰레드를 사용하는 작업을 진행할 시에 초기화가 여러 번 일어날 수 있다.
  • 멀티 쓰레드를 사용하는 작업을 진행할 시에 데이터 수정에서 데이터 레이스가 일어날 수 있다.

Go에서의 싱글톤 사용법

객체 선언, 초기화, 객체 불러오기

Bolt DB 객체를 생성하거나 불러오는 역할을 하는 함수를 살펴보자.

var db *bolt.DB
func DB() *bolt.DB {
    if db == nil {
        dbPointer, err := bolt.Open("test.db", 0600, nil)
        db = dbPointer
        utils.HandleErr(err)
        err = db.Update(func(t *bolt.Tx) error {
            _, err := t.CreateBucketIfNotExists([]byte(dataBucket))
            fmt.Println(err)
        })
    }
    return db
}

객체를 외부에서 직접 제거할 수 없도록 private 한 db객체를 전역으로 만든 모습을 볼 수 있다. public 한 DB 함수를 제작하여 어디서든 DB 객체를 불러올 수 있도록 제작하였다. 이 함수에서 DB에 nil이 있을 경우 즉, 한 번도 초기화되지 않은 상태에서는 DB 데이터를 DB 객체에 주입시켜주는 모습을 볼 수 있다. 만약 nil이 아니라면 이미 초기화되어 있는 상태라는 뜻이기에 바로 DB 객체를 반환한다.

이런 식으로 DB 객체를 사용한다면 위에서 말했듯 불필요하게 새로운 객체를 만들어 낼 필요가 없어진다. 또한 DB를 수정해야 할 때에는 전역으로 선언된 DB객체를 불러 간단히 수정하는 함수를 제작하면 된다.

단점 극복 방법

Go의 최대 장점이라 한다면 싱글톤에서 문제가 되는 다중 쓰레드를 Go의 내장 기능인 고루틴을 통해 어떠한 언어보다 간편히 컨트롤할 수 있는 게 아닐까 싶다. (

아직 나는 내공이 부족해서 정확히 잘 알지는 못하지만^^

)
그래서 아주 간단히 단점으로 언급한 것들에 대한 대비책을 말해 보겠다.

var db *bolt.DB
var once sync.Once // 추가된 부분

func DB() *bolt.DB {
once.Do(func() { // 추가된 부분
    if db == nil {
        dbPointer, err := bolt.Open("test.db", 0600, nil)
        db = dbPointer
        utils.HandleErr(err)
        err = db.Update(func(t *bolt.Tx) error {
            _, err := t.CreateBucketIfNotExists([]byte(dataBucket))
            fmt.Println(err)
        }) 
    }
}) // 추가된 부분
return db

따로 해 주어야 할 것이 많이 없는 것을 볼 수 있다. sync.Once를 사용하기 위한 객체를 만들어 주었고 이 객체가 가진 Do라는 함수를 사용하면 그만이다. 이게 무엇을 하는 친구냐.. 잘 모른다.. 그럼 또 Go의 장점이 무엇이냐 하면 엄청 정성스럽게 적혀있는 주석 아니겠는가!! 보자. 물론 영어다.

Do calls the function f if and only if Do is being called for the first time for this instance of Once.
Do는 이 Once 인스턴스에 대해 Do가 처음 호출되는 경우에만 함수 f를 호출합니다.

객체당 딱 한번만 실행하게 해주는 함수이다.
어떻게 이게 가능한가?
데이터 레이스를 극복하는 방법을 살펴보면 어느 정도 파악이 될 것이다.

type dbs struct {
    db *bolt.DB
    m  sync.Mutex
}

var db *dbs

sync라이브러리에는 golang의 고루틴을 보조할 수 있는 다양한 기능이 있다. 그중 자주 사용하게 될 친구 같다. 바로 Mutex이다. 여러 쓰레드가 하나의 자원을 공유할 때 이 Mutex는 자원에 대한 data race가 생기는 것을 방지해 주는 역할을 한다. 전공자이거나 OS공부를 해 본 분이라면 이름은 들어본 적 있을 것이다. 사용 방법 역시 간단히 살펴보자.

type dbs struct {
    db *bolt.DB
    m  sync.Mutex
}

var db *dbs

func test(){
    db.m.Lock()
    defer db.m.Unlock()
    // 여러개의 고루틴이 공유하는 자원을 처리하는 함수
}

Once객체 역시 이 Mutex를 가지고 있는 것이다.
너무 쉽다.

사실 쉽진 않다. 가져다 쓰기는 너무 쉽다.

Mutex를 통해 Lock을 걸어 하나의 고루틴만이 이 함수에 접근할 수 있게 한 것이다. 이렇게 간단한 코드 몇 줄 추가로 Data race를 방지할 수 있다. 이 얼마나 매력적인 언어인가.

마무리

간단한 예제를 통해서 GoLang에서의 Singlton Pattern을 적용한 것을 보였다.
이해하기 위해선 더 많은 것을 설명해야 하지만 그 에 대한 포스팅은 따로 하도록 하겠다.
부족하지만 이 글을 보는 누군가에게 도움이 됐으면 한다.
물론 내가 기억하기 위한 이유도있지만.
-끗-

'Study > Design Pattern' 카테고리의 다른 글

[OOP] 객체지향 설계 5원칙 SOLID (SRP, OCP)  (0) 2022.08.13
SOLID  (0) 2022.03.18
[Design Pattern] 어댑터 패턴(Adapter Pattern)  (0) 2021.08.26