소프트웨어 디자인 패턴에서 싱글턴 패턴
(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다.
주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다. - wikipedia

 

싱글턴 패턴의 등장 배경에는 아래와 같은 이유가 있다.

유일한 인스턴스 유지:
일부 시스템에서는 특정 클래스의 인스턴스가 하나만 존재해야 할 때가 있습니다. 예를 들어, 시스템의 설정 정보를 담당하는 클래스, 로그를 관리하는 클래스, DB 연결을 관리하는 클래스 등이 있습니다. 이런 경우, 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 존재하도록 보장합니다.

전역 접근: 또한 싱글턴 패턴은 이 유일한 인스턴스에 대한 전역적인 접근 방법을 제공합니다. 즉, 어디서든 이 인스턴스를 참조할 수 있게 됩니다. 이는 애플리케이션 전반에서 공유해야 하는 데이터나 리소스를 관리하는 데 유용합니다.

제어된 리소스 공유: 한정된 리소스를 공유해야하는 경우에도 싱글턴 패턴이 유용합니다. 예를 들어, 데이터베이스 연결풀, 파일 시스템, 네트워크 소켓 등의 리소스는 한정되어 있기 때문에 효율적으로 관리하고 사용할 필요가 있습니다.

메모리 및 성능 최적화: 싱글턴 패턴을 사용하면 메모리 사용을 최적화하고 성능을 향상할 수 있습니다. 인스턴스를 한 번만 생성하므로 메모리 낭비를 줄이고, 계속해서 같은 인스턴스를 재사용함으로써 성능을 향상할 수 있습니다.

 

예전 Java 에서 Singleton 패턴에 관련해 정리된 내용이 있어 Go에서는 어떻게 활용하는가? 에 초점을 두어 작성하겠다.

(https://guiwoo.tistory.com/24)

 

전통적인 방식

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func GetInstance() *db {
	if obj == nil {
		obj = &db{}
	}
	return obj
}

 

위와 같이 구현을 한다. 다만 이렇게 구현 되었을때 문제점이 발생된다. 바로 멀티스레드 환경인데 

(생성 과정 의 단순함 과 Go Routine의 최적화에 따라 문제점 발생이 너무 희박해서 강제로 상황을 만들었다 "runtime.Gosched()를 활용")

func main() {
	wg := &sync.WaitGroup{}

	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			singleton.GetInstance()
		}()
	}
	wg.Wait()
}

DB의 인스턴스를 여러 번 생성하는 것을 확인할 수 있다. 

 

이를 방지하기 위해 Go 에서 제공해 주는 특별한 기능들이 있는데

 

Sync.Once(https://pkg.go.dev/sync)

- 싱크 패키지 에서 제공하는 구조체 중에 하나로 Once는 한 번만 수행되도록 도와주는 구조체이다. 제공되는 메서드 중 하나로 Do 메서드가 있으며 Do에서 실행되는 콜백 function 은 오직 한 번의 실행만을 수행한다.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db
var o sync.Once

func GetInstance() *db {
	o.Do(func() {
		runtime.Gosched()
		obj = &db{}
		fmt.Println("Crated DB Instance")
	})
	return obj
}

동일한 main func 을 수행한다면?

위와 같은 결과를 얻을 수 있다. 

 

init 함수 활용 하기

- go에서는 패키지 import 간 최초 실행 해주는 함수가 있다. 바로 init() 함수인데 제일 좋아하는 방법이다. 바로 활용해 보자.

type db struct {
	/**
	db 에 해당하는 값 이 저장
	*/
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("Db created")
}

func GetInstance() *db {
	if obj == nil {
		fmt.Println("Three is no db instance")
	}
	return obj
}

개인적으로는 init 함수를 선호한다. 

 

이러한 싱글턴 디자인 패턴 에는 테스트의 어려움 이 있다. 변수를 아무래도 전역적으로 관리하다 보니 목킹 이 생각보다 어렵다. 

이런 경우 인터페이스를 활용해서 추상화 작업을 통해 작성하는 것도 하나의 방법이 될 수 있다.

 

type MyDB interface {
	DoSomething() // DB 작업을 위한 메서드
}

type db struct {
	// db에 해당하는 값이 저장
}

var obj *db

func init() {
	obj = &db{}
	fmt.Println("DB created")
}

func (d *db) DoSomething() {
	// DB 작업을 수행하는 메서드
}

func GetInstance() MyDB {
	if obj == nil {
		fmt.Println("There is no DB instance")
	}
	return obj
}

 

+ Recent posts