Go 패키지 언패킹: 정의, 구조 및 가져오기 메커니즘
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go의 우아한 코드 구성 방식은 "패키지"라는 개념을 중심으로 이루어집니다. 패키지는 Go 프로그램의 구성 요소로서, 모듈성, 재사용성 및 캡슐화를 위한 메커니즘을 제공합니다. 관련 기능을 그룹화하여 대규모 프로젝트를 관리 가능하게 하고 협업을 촉진합니다.
Go 패키지란?
핵심적으로 Go 패키지는 하나 이상의 Go 소스 파일(.go
파일)을 포함하는 디렉토리일 뿐입니다. 동일한 디렉토리 내에서 package <packagename>
을 상단에 선언하는 모든 파일은 해당 패키지에 속합니다.
패키지 선언
모든 Go 소스 파일은 파일 맨 처음에 package
키워드와 패키지 이름을 사용하여 자신이 속한 패키지를 선언해야 합니다.
// my_package/greeter.go package my_package // 이 파일을 'my_package'의 일부로 선언 func Greet(name string) string { return "Hello, " + name + " from my_package!" }
주요 패키지 유형:
-
main
패키지: 실행 가능한 프로그램을 정의하는 특별한 패키지입니다. Go 프로그램은 정확히 하나의main
패키지를 가져야 하며, 프로그램의 진입점인main
함수를 포함해야 합니다.// main.go package main // 실행 가능한 패키지로 선언 import "fmt" func main() { fmt.Println("This is the main executable.") }
-
명명된 패키지 (Non-
main
): 이 패키지들은 라이브러리이거나 다른 패키지(main 패키지 포함)에서 가져오고 사용할 수 있는 재사용 가능한 함수, 유형 및 변수의 모음입니다. 이름은 일반적으로 해당 파일이 속한 디렉토리와 동일합니다. 예를 들어,utils
라는 디렉토리가 있다면, 해당 파일 내에 선언된 패키지는package utils
여야 합니다.// utils/strings.go package utils // 'utils' 패키지로 선언 import "strings" func Reverse(s string) string { runes := []rune(s) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } return string(runes) } func ToUpper(s string) string { return strings.ToUpper(s) }
Exported vs. Unexported 식별자
Go에는 가시성을 위한 간단한 규칙이 있습니다:
- Exported 식별자: 이름이 대문자로 시작하는 모든 함수, 변수, 유형 또는 구조체 필드는 "export"되며, 이를 가져오는 다른 패키지에서 액세스할 수 있습니다.
- Unexported 식별자: 소문자로 시작하는 식별자는 "unexport"되며, 정의된 패키지 내에서만 볼 수 있습니다. 본질적으로 패키지에 비공개입니다.
이 규칙은 캡슐화를 촉진하고 의도하지 않은 부작용을 방지하는 데 도움이 됩니다.
// my_package/calculator.go package my_package var privateConstant = 10 // Unexported const PublicConstant = 20 // Exported func add(a, b int) int { // Unexported function return a + b } func Multiply(a, b int) int { // Exported function return a * b }
패키지 가져오기
다른 패키지의 기능을 사용하려면 import
해야 합니다. import
선언은 일반적으로 package
선언 후에, 다른 코드 앞에 나타납니다.
기본 가져오기
패키지를 가져오는 가장 일반적인 방법은 전체 가져오기 경로를 지정하는 것입니다. 표준 라이브러리 패키지의 경우 일반적으로 패키지 이름만 사용됩니다(예: "fmt"
, "math"
, "strings"
). 로컬 패키지 또는 타사 패키지의 경우 모듈 경로 다음에 디렉토리 경로가 옵니다.
// main.go package main import ( "fmt" // 표준 라이브러리 패키지 "strings" // 또 다른 표준 라이브러리 패키지 "my_project/utils" // 로컬 또는 타사 패키지 ( 'my_project'가 모듈 이름이라고 가정) ) func main() { fmt.Println(strings.ToUpper("hello go!")) fmt.Println(utils.Reverse("olleh")) }
가져올 때 Go는 다음 순서로 패키지를 검색합니다.
- 표준 라이브러리.
- Go 모듈의
vendor
디렉토리 (go mod vendor
사용 시). - Go 모듈의
pkg
디렉토리 (미리 컴파일된 패키지의 경우). - Go 모듈의 소스 디렉토리 (
go.mod
가 모듈 경로를 정의합니다).
패키지 이름 별칭 사용
때때로 가져온 두 패키지에 이름이 충돌할 수 있거나, 더 짧고 편리한 이름을 사용하고 싶을 수 있습니다. 가져오기 중에 패키지에 대한 별칭을 사용할 수 있습니다.
// main.go package main import ( "fmt" str "strings" // "strings" 패키지를 "str"이라는 별칭으로 지정 ) func main() { fmt.Println(str.ToUpper("aliased string")) }
Blank Import (_
)
Blank import는 순전히 부작용(예: 초기화 논리, 데이터베이스 드라이버 등록)을 위해 패키지를 가져올 필요가 있을 때 사용됩니다. 가져온 패키지의 init()
함수(있는 경우)가 실행되지만, export된 식별자는 직접 사용할 수 없습니다.
// database_drivers/postgres.go package database_drivers import "fmt" func init() { fmt.Println("PostgreSQL driver initialized!") // 데이터베이스 패키지로 드라이버 등록 } // main.go package main import ( "fmt" _ "my_project/database_drivers" // init() 실행을 위한 Blank import ) func main() { fmt.Println("Application starting...") // database_drivers 함수 직접 사용 안 함 }
Dot Import (.
)
Dot import는 가져온 패키지의 모든 export된 식별자를 현재 패키지의 네임스페이스에 직접 사용할 수 있게 하여, 패키지 이름으로 접두사를 붙일 필요가 없게 합니다. 코드를 더 짧게 만들 수 있지만, 이름 충돌을 일으킬 수 있고 함수나 변수가 어디서 왔는지 구분하기 어렵게 만들 수 있으므로 일반적으로 권장되지 않습니다.
// my_package/utils.go package my_package func SayHello() { fmt.Println("Hello from utils!") } // main.go package main import ( "fmt" . "my_project/my_package" // Dot import ) func main() { fmt.Println("Main application.") SayHello() // 'my_package.' 접두사 없이 직접 호출 }
패키지 초기화 (init()
함수)
각 패키지는 하나 이상의 init()
함수를 가질 수 있습니다. 이 함수들은 프로그램의 main()
함수가 실행되기 전, 그리고 모든 가져온 패키지가 초기화된 후에 Go 런타임에 의해 자동으로 실행됩니다. 동일한 패키지 내 또는 동일한 패키지의 다른 파일에 있는 여러 init()
함수는 파일의 사전순으로 실행되지만, 다른 패키지 간의 init()
함수 실행 순서는 보장되지 않으며, 단지 의존성이 먼저 초기화된다는 것만 보장됩니다.
// my_package/init1.go package my_package import "fmt" func init() { fmt.Println("my_package: init1 called") } // my_package/init2.go package my_package import "fmt" func init() { fmt.Println("my_package: init2 called") } // main.go package main import ( "fmt" _ "my_project/my_package" // my_package 가져오기 ) func init() { fmt.Println("main: init called") } func main() { fmt.Println("main: main function called") }
실행하면 다음과 유사한 출력이 나옵니다.
my_package: init1 called
my_package: init2 called
main: init called
main: main function called
( my_package
자체의 init 함수들 (init1
대 init2
)의 순서는 파일 발견 순서에 따라 달라질 수 있지만, 둘 다 main
의 init보다 먼저 실행될 것입니다.)
패키지 관리 모범 사례
- 의미있는 이름: 패키지의 목적을 반영하는 명확하고 간결한 이름을 선택합니다.
- 디렉토리 구조: 논리적인 디렉토리 계층 구조로 패키지를 구성합니다.
my_project/ ├── go.mod ├── main.go ├── models/ │ └── user.go ├── handlers/ │ └── user_handler.go └── utils/ └── string_utils.go
- 캡슐화: 필요한 기능만 노출하고 내부 구현 세부 정보를 비공개로 유지하기 위해 Go의 export 규칙을 활용합니다.
- 순환 종속성 방지: 패키지가 서로 순환적으로 의존해서는 안 됩니다(예: 패키지 A가 B를 가져오고, 패키지 B가 A를 가져오는 경우). 이는 컴파일 오류를 발생시킵니다.
- 최소한의 가져오기: 실제로 필요한 패키지만 가져옵니다. 사용되지 않은 가져오기는 컴파일 오류를 발생시킵니다. 이는 종속성을 깔끔하게 유지하고 컴파일 시간을 단축하는 데 도움이 됩니다.
- 모듈 시스템 (
go mod
): 외부 종속성을 관리하고 프로젝트의 모듈 경로를 정의하려면 항상 Go 모듈 시스템 (go mod init
,go get
)을 사용합니다. 이는 버전 관리 및 재현 가능한 빌드를 제공합니다.
Go의 패키지 시스템을 이해하고 효과적으로 활용하면 개발자는 잘 구성된 유지 관리 가능하고 확장 가능한 애플리케이션을 작성할 수 있습니다.