![Golang init() 사용법 및 주의 사항](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyPOm%2FbtsHsRzyqe3%2FX7hraT4laKi8JxLhQSmGmK%2Fimg.jpg)
Golang Init의 기본 순서
Init 함수는 기본적으로 Application의 상태를 정의하기 위해서 사용합니다.
어떠한 Argument를 받거나 return 값을 제공하지도 않습니다.
패키지가 처음 불려지면 그때 모든 상수나 변수가 계산되고 그 이후에 Init() 함수가 실행됩니다.
간략하게는 아래 순서로 실행됩니다.
import --> const --> var --> init()
조금 더 자세히 설명하자면 아래와 같습니다.
- 만약 패키지가 다른 패키지를 import하면, import한 패키지의 init() 및 initialize 과정이 먼저 실행됩니다.
- 현재 패키지의 constant가 initialize 됩니다.
- 현재 패키지의 variable이 initialize 됩니다.
- 최종적으로 현재 피키지의 init() 함수가 실행됩니다.
만약 패키지가 여러 init() 함수를 가지고 있다면, 컴파일러에 노출되어 있는 순서대로 실행이 됩니다.
(예를들면 settings package에 set_hardware.go::init(), set_software.go::init() 이렇게도 생성할 수도 있는데, 이 경우 컴파일러가 보고있는 순서, 즉 파일 이름의 알파벳 순으로 하게됩니다. 이 사실을 잘 인지 하지 않으면 순서를 파악하기가 쉽지 않죠.)
그리고 여러 패키지에서 특정 패키지를 import 한다고 하더라도 그 특정 패키지는 한번만 initialize 됩니다.
Init 사용시 주의사항
Init()의 기능적 한계
1. 별도 에러 결과를 반환할 수 없어서 에러처리에 한계가 있습니다.
일반 함수에 비해서 init() 함수는 별도로 error를 리턴 할 수 없어서 panic()을 통해서 에러 처리를 할수밖에없습니다.
func init() {
dataSourceName :=
os.Getenv("MYSQL_DATA_SOURCE_NAME")
d, err := sql.Open("mysql", dataSourceName)
if err != nil {
log.Panic(err)
}
err = d.Ping()
if err != nil {
log.Panic(err)
}
db = d
}
그래서 init()을 통한 에러처리 대처 방법은 아래 4가지 정도 가능합니다.
2. 유닛 테스트 작성에 유의가 필요합니다. 특히! 외부 의존성이 init()에 주입이 되어있으면, unit test에서는 오류를 발생시키거나, 의도치 않은 동작을 할 수 있습니다.
예를 들어 아래와 같이 database 패키지에서 db를 initialize 하는 동작을 init() 함수로 정의했습니다.
package database
var DB *gorm.DB
func init() {
databaseFile := "real_database.db" // 데이터베이스 파일명을 지정합니다.
// GORM을 이용하여 SQLite 데이터베이스 연결을 초기화합니다.
db, err := gorm.Open(sqlite.Open(databaseFile), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
DB = db
log.Println("Database connection successfully established with SQLite")
}
// GetDB is a helper function to access the database connection globally
func GetDB() *gorm.DB {
return DB
}
이 init() 함수에는 실제 deploy 될 수 있는 파일인 real_database.db 파일이 경로로 지정되어있는데요.
이런 경우에 database package의 Unit Test에서는 해당 경로의 db를 사용하고 싶지 않는 경우가 대부분인데
UT에서도 동일한 DB를 써버리게 됩니다.
배포 환경에서는 UT가 이런 경로를 쓰는게 문제는 안되겠지만, 개발 환경에서 테스트할때는 UT를 실행할때마다 실제로 확인하고 있는 DB가 업데이트 되어 문제가 생길 수 있겠죠.
이런 부분들을 조심하면서 init() 함수를 짜야합니다.
3. init 과정이 어떤 상태를 정의해야한다면, 반드시 전역 변수를 사용할수밖에 없습니다.
이 경우의 문제점은
1. 패키지 안의 어떤 함수든 전역 변수를 변경할 수 있다는점
2. 1번으로 인해서 전역변수가 더이상 isloated 되어 있지 않아 unit-test 작성이 문제가 생길 수 있다는 점입니다.
예를 하나 들어보겠습니다.
아래와 같이 전역 변수로 Settings라는 변수를 정의했습니다.
그리고 해당 변수에는 "info" 라는 log level이 할당되어있죠.
package go_playground
type Settings struct {
LogLevel string
}
var settings *Settings
func init() {
settings = &Settings{
LogLevel: "info",
}
}
func SetInstance(setting *Settings) {
settings = setting
}
func GetLogLevel() string {
return settings.LogLevel
}
아래와 같이 SetInstance()와 GetLogLevel()을 독립적으로 테스트 하려고 함수를 만들었는데요.
SetInstance에 nil을 할당해봤습니다.
package go_playground
import (
"testing"
)
func TestSetInstance_AssignNil(t *testing.T) {
SetInstance(nil)
}
func TestGetLogLevel_ExpectedInfo(t *testing.T) {
logLevel := GetLogLevel()
if logLevel != "info" {
t.Errorf("Expected log level to be 'info', got %s", logLevel)
}
}
각각에 대해서 단일 테스트만 실행을 하면 위 두개의 테스트는 실패하지 않습니다.
그런데 패키지의 UT를 전부 실행하면 아래와 같이 실패합니다.
=== RUN TestSetInstance_AssignNil
--- PASS: TestSetInstance_AssignNil (0.00s)
=== RUN TestSetLogLevel_GetLogLevel
--- FAIL: TestSetLogLevel_GetLogLevel (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x1b8b61]
goroutine 7 [running]:
testing.tRunner.func1.2({0x1ccd00, 0x2d6c00})
C:/Program Files/Go/src/testing/testing.go:1526 +0x24e
testing.tRunner.func1()
C:/Program Files/Go/src/testing/testing.go:1529 +0x39f
panic({0x1ccd00, 0x2d6c00})
C:/Program Files/Go/src/runtime/panic.go:884 +0x213
go_playground.GetLogLevel(...)
C:/Users/Sanghyeok/Desktop/1.Project/DanawaCrawler/danawa-crawler/go_playground/config.go:20
go_playground.TestSetLogLevel_GetLogLevel(0xc000047860)
C:/Users/Sanghyeok/Desktop/1.Project/DanawaCrawler/danawa-crawler/go_playground/config_test.go:12 +0x21
testing.tRunner(0xc000047860, 0x1f7538)
C:/Program Files/Go/src/testing/testing.go:1576 +0x10b
created by testing.(*T).Run
C:/Program Files/Go/src/testing/testing.go:1629 +0x3ea
Process finished with the exit code 1
왜냐면 SetInstance(nil)이 전역변수를 nil로 초기화 해 버려서 이 전역변수를 사용하는 다른 테스트에까지 영향을 주었기 때문이죠.
패키지 내의 init()은 단 한번만 실행되기에 전역변수를 변경시키는 함수를 테스트하는 경우 다른 Unit-Test에 영향을 줄 수 있음을 알 수 있었습니다.
실제로 프로젝트를 진행함에 있어서 위와 같은 문제들을 동일하게 겪을 수 있었는데요.
위의 예시는 처리하기가 쉬울 수 있지만, 프로젝트가 복잡하면 복잡 할수록 init() 로직을 잘못 만들었을때 수정하기가 더 어려웠습니다.
이를 미리 파악했었더라면 코드를 더 수월하게 짤 수 있었겠다라는 생각이 들어 본 포스트를 작성했습니다.
도움이 되셨으면 좋겠고, 저와 같은 실수를 하지 않기를 바랍니다.
참고
Understanding init in Go | DigitalOcean
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
www.digitalocean.com
Handle errors in init
I have an init func in a package. Inside the init function, I have a database initialization which returns error. How can I handle that error failure incase if it fails in the init function. Thank you Nikhilesh
groups.google.com
Common Go Mistakes - 100 Go Mistakes and How to Avoid Them
Summary of the mistakes in the 100 Go Mistakes book.
100go.co
'백엔드 > Golang' 카테고리의 다른 글
Golang Convention 중 논의를 해야 할 사항 정리 (0) | 2024.11.10 |
---|---|
Golang 에러 처리 - (1) Google Guide/Best Practice 찾아보기 (2) | 2024.09.07 |
Golang zero-value 알아보기 (0) | 2024.07.09 |
[번역] Golang vs Spring boot native 성능 비교해보기 - Hello world 케이스 (0) | 2024.07.08 |
우리 프로젝트에서 Golang DB 처리 시에 GORM을 사용 해야 하는 이유 (0) | 2024.05.01 |
개발 및 IT 관련 포스팅을 작성 하는 블로그입니다.
IT 기술 및 개인 개발에 대한 내용을 작성하는 블로그입니다. 많은 분들과 소통하며 의견을 나누고 싶습니다.