9장 모듈, 패키지 그리고 임포트

2024.04.17

9.1 저장소, 모듈 그리고 패키지

  • Go에서 라이브러리 관리는 저장소, 모듈 그리고 패키지라는 개념에 기반한다.
  • 저장소는 모든 개발자에게 익숙하다.
  • 모듈은 저장소에 저장된 Go 라이브러리나 응용 프로그램의 최상위 루트이다.
  • 모듈은 모듈 구성 및 구조를 제공하는 하나 이상의 패키지로 구성되어 있다.

저장소에 하나 이상의 모듈을 저장할 수는 있지만, 권장하지는 않는다.

모듈 내에 있는 모든 것은 함께 버전 지정이 된다.

9.2 go.mod

  • Go 소스 코드의 컬렉션은 해당 루트 디렉터리에 유효한 go.mod 파일이 있을 때 모듈이 된다.
  • go mod init MODULE_PATH 명령어는 현재 디렉터리를 모듈의 루트로 만드는 go.mod 파일을 생성한다.
module github.com/learning-go-book/money

go 1.15

require (
    github.com/learning-go-book/formatter v0.0.0-20200921021027-5abc380940ae
)
  • 모든 go.mod 파일은 module 이라는 단어와 모듈의 유일한 경로로 구성되는 module 선언으로 시작된다.
  • go.mod 파일은 Go의 최소 호환 버전을 지정한다.
  • require 섹션은 해당 모듈이 의존하는 모듈과 각 모듈에 필요한 최소 버전이 나열된다.

9.3 패키지 빌드

9.3.1 가져오기와 노출시키기

  • Go의 import 문은 다른 패키지에서 노출된 상수, 변수, 함수, 타입을 접근할 수 있도록 한다.
  • Go에서는 패키지 레벨 식별자를 선언된 패키지 외부에서 보여지는 것을 결정하기 위해 대문자를 사용한다.
    • 이름이 대문자로 시작하는 식별자는 노출된 것이다.
  • 반대로, 이름이 소문자나 밑줄(_)로 시작하는 식별자는 선언된 패키지 내에서만 접근이 가능하다.

9.3.2 패키지 생성과 접근

package math

func Double(a int) int {
    return a * 2
}
  • 파일의 첫 번째 라인을 패키지 절(package clause)라 한다. ( package 키워드와 패키지 이름으로 구성 )
import (
  "fmt"
  "github.com/learning-go-book/package_example/formatter"
  "github.com/learning-go-book/package_example/math"
)
  • 위에서는 3개의 패키지를 가져왔다. 첫 번째는 표준 라이브러리에 있는 fmt이다. 다음 두 개는 해당 프로그램에 있는 패키지를 참조한다.
  • 표준 라이브러리를 제외한 다른 모든 곳에서 가져오는 경우에는 임포트 경로를 반드시 지정해야 한다.
    • 임포트 경로는 모듈에 있는 패키지 경로를 모듈 경로에 추가하여 만들어진다.
  • 패키지에서 노출된 어떤 식별자도 사용하지 않는다면 패키지를 import로 가져오는 것은 컴파일 오류를 발생시킨다. 이것은 Go 컴파일러가 만들어내는 바이너리는 프로그램에서 실제 사용되는 코드만 포함한다는 것을 보장한다.
  • 일반적으로 패키지를 포함하는 디렉터리 이름과 일치하는 패키지 이름을 만들도록 하자. 포함하는 디렉터리와 이름이 다르다면 패키지 이름으로 찾아내기가 어려울 수 있다.

9.3.3 패키지 이름 재정의

  • 패키지 이름은 설명적이어야 한다.
    • util이라는 패키지 이름을 가지는 것보다 패키지가 제공하는 기능을 설명하는 패키지 이름을 생성하자.
    • util 패키지에 ExtractName과 FormatName으로 만들지 말고, extract라 불리는 패키지에 Names라는 함수를 생성하고 다른 하나는 format 패키지에 Names라는 함수를 만드는 것이 좋다.

9.3.4 모듈을 구성하는 방법

  • 모듈에서 Go 패키지를 구성하는 공식적인 방법은 없지만 몇 년에 걸쳐 몇 가지 패턴들이 나타났다.
  • 모듈이 작을 때, 모든 코드를 단일 패키지에 유지하도록 하자. 자신의 모듈에 의존하는 다른 모듈이 없는 한, 구성을 지연하더라도 아무런 해가 없다.
  • 모듈이 하나 이상의 응용 프로그램으로 구성되었다면, 모듈의 루트 디렉터리에 cmd라는 디렉터리를 만들자. cmd 내에서는 모듈에서 생성하는 각 바이너리에 대해 하나의 디렉터리를 만든다.
  • Go 프로젝트 구조의 조언에 대한 좋은 개요를 보려면 How Do You Structure Your Go Apps를 시청해보도록 하자.

9.3.5 패키지 이름 재정의

  • 때로는 중복된 이름의 두 패키지를 가져와야 하는 경우가 있다.
  • 예를 들어, 하나는 암호학적으로 안전한(crypto/rand)것이고 다른 하나(math/rand)는 그렇지 않은 것이다. 이 두 패키지는 모두 같은 이름(rand)을 가지고, 함께 사용하는 일이 발생한다면, 하나의 패키지에 대체 이름을 부여해야 한다.
import (
  crand "crypto/rand" // crypto/rand를 crand라는 이름으로 가져왔다.
  "encoding/binary"
  "fmt"
  "math/rand"
)

9.3.6 패키지 주석과 godoc

  • Go는 자동적으로 문서로 변환해주는 주석을 작성하기 위한 Go만의 포맷을 가진다.
  • godoc 주석에는 특별한 심볼은 없다. 단지 아래와 같은 관례에 따라 진행된다.
    • 항목의 선언과 주석사이에 빈 줄이 없이 문서화가 될 항목 전에 바로 주석을 작성한다.
    • 두 개의 슬래시와 항목의 이름으로 주석을 시작한다.
    • 여러 단락으로 주석을 나누기 위해서는 빈 주석라인을 사용한다.
    • 라인을 들여쓰기 하여 미리 서식이 지정된 주석을 추가할 수 있다.
  • Go는 godoc을 보여주는 go doc 이라 불리는 명령라인 도구를 포함한다.

최소한 모든 노출된 식별자에는 주석이 있어야 한다.

golint와 golangci-lint와 같은 Go의 린트 도구는 주석이 없는 노출된 식별자를 보고한다.

9.3.7 내부 패키지

  • 때로는 모듈에서 패키지 간에 함수, 타입, 상수를 공유하고 싶지만, API의 일부가 되게는 하고 싶지 않는 경우가 있을 것이다. Go는 특별한 internal 패키지 이름을 통해 이것을 지원한다.
├── bar/
│   └── bar.go
├── example.go
├── foo/
│   ├── foo.go
│   ├── internal/
│   │   └── internal.go
│   └── sibling/
│       └── sibling.go
└── go.mod
  • bar 패키지의 bar.go 파일이나 루트 패키지에 있는 example.go에서 internal 패키지에 있는 함수를 사용하려고 했을 때는 다음과 같은 컴파일 오류가 발생한다.

9.3.8 init 함수: 가능하면 피하자

  • Go는 단일 패키지 내에서 여러 init 함수를 선언할 수 있도록 하고, 여러 init 함수는 작성된 순서대로 실행이 되지만, 그것을 기억하는 것보다는 사용하지 않는 것이 좋다.
  • Go는 사용되지 않는 가져온 패키지를 허용하지 않는다. 이것을 해결하기 위해, import 문의 가져오는 패키지 앞에 밑줄(_)을 이름으로 할당하여 공백 가져오기(blank import)를 허용한다.
import (
    "database/sql"

    _ "github.com/lib/pq"
)
  • 이런 패턴은 등록 작업이 수행되고 있는지 명확하지 않기 때문에 더 이상 사용되지 않는 것으로 간주된다.
  • init을 통해 어떤 패키지 레벨 변수를 설정하는 것은 효과적으로 변경할 수 없다는 것이다. Go는 값을 변경할 수 없도록 강제하는 기능을 제공하지 않기 때문에, 코드가 그것을 변경하지 않도록 해야 한다.
  • 패키지 내에서 함수로 초기화 하고 반환되는 구조체에 해당 상태를 넣을 수 있는지 확인하자.

9.3.9 순환 의존성

  • Go의 목표 중 두 개는 빠른 컴파일러와 코드를 쉽게 이해할 수 있게 하기 위해, Go는 패키지들 간에 순환 의존성을 가지게 허용하지 않는다.
  • 순환 의존성이 있는 프로젝트를 빌드하면, 오류를 볼 수 있다.

9.3.10 API의 이름을 우아하게 바꾸고 재구성하기

  • 한동안 모듈을 사용하다 보면 해당 API가 이상적이지 않다는 것을 깨닫게 된다. 노출된 식별자의 일부의 이름을 바꾸길 원하거나 그것들을 당신의 모듈 내에 다른 패키지로 이동시키고 싶은 것이다. 기존 호환성을 깨는 변경을 피하기 위해, 원본 식별자를 제거하는 대신에 대체 이름을 제거하자.
type Foo struct {
  x int
  S string
}

func (f Foo) Helllo() string {
    return "hello"
}

func (f Foo) goodbye() string {
    return "goodbye"
}
  • 사용자가 Bar라는 이름으로 Foo를 접근하는 것을 원한다면, 다음과 같이 할 수 있다.
type Bar = Foo
  • 기억해야할 중요한 한가지 지점은 별칭은 단지 타입을 위한 다른 이름이다. 별칭의 구조체의 항목의 변경이나 새로운 메서드를 추가하려면, 원본 타입에다 해야 한다.
  • 대체 이름을 가질 수 없는 노출된 식별자의 두 가지 종류가 있다.
    • 패키지 레벨 변수
    • 구조체의 항목

9.4 모듈 관련 작업

9.4.1 서드-파티 코드 가져오기

  • 다른 많은 컴파일 언어와는 다르게 Go는 응용 프로그램을 위한 서드-파티에서 가져온 코드와 본인이 작성한 코드 모두를 컴파일하여 단일 바이너리로 만든다. 자신의 프로젝트내에 있는 패키지를 가져올 때 보았듯이 서드-파티 패키지를 가져올 때, 패키지가 있는 소스 코드 저장소의 위치를 지정하면 된다.
  • 의존성이 필요한 go 명령(go run, go build, go test , go list 와 같은)을 실행할 때마다 go.mod 에 아직 기록되지 않은 임포트 모듈이 캐시에 다운로드 된다.

9.4.2 버전 작업

  • 기본적으로 Go는 모듈을 프로젝트에 추가하면 의존성의 최신버전을 선태한다. 하지만 모듈의 이전 버전도 선택이 가능하다는 것이 버전 관리를 유용하게 만든다.
  • go list 명령어를 통해 모듈의 어떤 버전이 가능한 것인지 확인할 수 있다.
go list -m -versions github.com/learning-go-book/simpletax

// github.com/learning-go-book/simpletax v1.0.0 v1.1.0
// 2가지 버전 존재
go get github.com/learning-go-book/simpletax@1.0.0
  • go get 명령어는 모듈과 관련된 작업과 의존성 업데이트를 할 수 있도록 한다.

9.4.3 최소 버전 선택

  • 어떤 지점에서는 프로젝트가 같은 모듈에 의존하는 2개 이상의 모듈에 의존성을 가질 수 있다.
  • Go의 모듈 시스템은 최소 버전 선택의 원칙을 사용한다. go.mod 파일에서 기록되어 가져오게 될 선언된 의존성들을 만족할 수 있는 가장 낮은 버전을 가져오도록 한다.

9.4.4 호환되는 버전으로 업데이트

  • go get -u=patch {주소} : 현재 부 버전을 기준으로 버그 수정에 대한 업그레이드를 위한 명령
  • go get {주소}@version : 특정 버전을 설치하기 위한 명령
  • go get {주소} : 가장 최신 버전을 설치하기 위한 명령

9.4.5 호환되지 않는 버전으로 업데이트

  • 임포트 문에 버전을 명시하도록 변경하여 특정 버전의 모듈을 참조하도록 한다.
"github.com/learning-go-book/simpletax/v2"
  • go build 를 실행하면 해당 의존성은 자동으로 업데이트된다. 이때 go.mod 파일에도 새로운 버전이 포함된 것을 볼 수 있다. ( go.sum 파일도 업데이트 되어 있다. )
module region_tax

go 1.15

require (
    github.com/learning-go-book/simpletax v1.0.0 // indirect

    github.com/learning-go-book/simpletax/v2 v2.0.0
)
  • 이전 버전은 더 이상 사용하지 않더라도 여전히 참조가 된다. 이것은 어떤 문제를 일으키진 않는다.
  • 또한, Go는 사용되지 않는 버전을 제거하는 명령어도 갖고 있다.
go mod tidy

9.4.6 벤더링

  • 모듈이 항상 동일한 의존성과 빌드되는 것을 보장하기 위해, 어떤 조직에서는 해당 모듈 내에 의존성의 복사본을 유지하기를 선호한다. 이를 벤더링(vendoring)이라고 한다.
  • go mod vendor 명령을 수행하여 활성화시킨다. 이것은 모듈의 최상위 디렉터리에 모듈이 가지는 의존성 모두를 포함하는 vendor라는 디렉터리를 생성한다.
  • 벤더링의 이점은 프로젝트에서 사용되는 서드-파티 코드에 관해 정확히 알고 있다는 것이다. 단점은 버전 관리되는 프로젝트의 크기가 엄청나게 커진다는 것이다.

9.5 모듈 게시

  • 모듈을 깃허브와 같은 버전 관리 시스템에 올려 놓으면 다른 사람이 당신의 모듈을 사용할 수 있다.
  • Go 프로그램은 소스코드로 부터 빌드되고 소스 코드를 식별하기 위해 저장소 경로를 사용하기 때문에, 메이븐 센트럴이나 npm을 위해 하는 것처럼 중앙 라이브러리 저장소에 당신의 모듈을 명시적으로 업로드 할 필요가 없다.

9.6 모듈 버전 관리

  • 기능을 추가하거나 버그를 수정하는 한, 해당 과정은 시멘틱 버전 관리 규칙에 따라 태그를 적용하면 단순하다.
  • 하위 호환을 꺨 필요가 있는 변경 지점에 도달한다면, 과정은 더 복잡 해진다.
  • 하위 호환을 깨는 변경은 다른 임포트 경로를 요구한다.
  • Go는 다른 임포트 경로를 생성하는 두 가지 방법을 지원한다.
    • 모듈 내에 vN 이라는 하위 디렉터리를 생성한다. ( ex) v1, v2 )
    • 버전 관리 시스템에서 새로운 브랜치를 생성한다. 브랜치 이름을 vN 으로 지정한다.
  • 새로운 코드를 저장하는 방법을 결정한 뒤에 하위 디렉터리나 브랜치의 코드에서 임포트 경로를 변경할 필요가 있다.
  • 새 코드를 게시할 준비가 되었다면, vN.0.0 과 같은 태그를 저장소에 적용하고, 브랜치를 태깅하면 된다.

9.7 모듈을 위한 프록시 서버

  • 라이브러리를 위한 단일 중앙 집중의 저장소에 의존하는 대신에, Go는 하이브리드 모델을 사용한다.
  • 모든 Go 모듈을 깃허브와 깃랩과 같은 소스 코드 저장소에 저장된다. 하지만 기본적으로 go get 은 소스 코드 저장소에서 직접 가져오지는 못한다. 해당 명령은 구글에서 수행하는 프록시 서버로 요청한다. 해당 서버는 거의 모든 공개 Go 모듈의 모든 버전을 복사하여 보관한다.
  • 프록시 서버 외에도 구글은 집계 데이터베이스(sum database)도 유지 관리한다.
    • 모든 모듈의 모든 버전 정보를 저장한다. ( go.sum 파일에 보여지는 모듈의 버전과 체크섬도 포함 )
  • 프록시 서버가 인터넷에서 제거되는 모듈이나 버전으로부터 사용자를 보호하는 것과 같이 집계 데이터베이스는 모듈의 수정으로부터 사용자를 보호한다.
    • 누군가 모듈을 가로채 악의적인 코드를 넣거나 모듈 관리자의 부주의로 버그 수정시 버전 태그를 재사용하는 경우 등
  • 매번 go build, go test , go get 을 이용해 모듈을 다운로드 받을 때마다, Go 도구는 모듈을 위한 해시를 계산하고 집계 데이터베이스에 접근하여 모듈 버전을 위한 저장된 해시와 계산된 해시를 비교한다. 만약 두 값이 일치하지 않으면 모듈은 설치되지 않는다.

9.7.1 프록시 서버 지정하기

  • 구글의 프록시 서버 사용을 원치 않는다면, GOPROXY 환경 변수를 GoCenter로 전환할 수 있다.
  • GOPROXY 환경 변수의 값을 direct 로 설정하여 프록시 서버를 완전히 비활성화 할 수 있다.
  • 직접 프록시 서버를 수행할 수 있다.
    • Artifactory와 Sonatype은 해당 제품의 엔터프라이즈 저장소 제품에 Go 프록시 서버를 포함한다. 이런 제품을 네트워크 내에 설치하여 GOPROXY 에 해당 URL로 가리키게 하면 된다.

9.7.2 비공개 저장소

  • 대부분의 조직은 조직의 코드를 비공개 저장소에 보관한다. 다른 Go 프로젝트 내에 비공개 모듈 사용을 원한다면, 구글 프록시 서버로 해당 모듈을 요청할 수 없다.
  • 자체 프록시 서버를 사용하거나 프록시를 비활성화했다면, 문제가 되지 않는다.
  • 공개 프록시 서버를 사용한다면 GOPRIVATE 환경 변수에 쉼표로 구분된 비공개 저장소의 목록을 설정할 수 있다.
GOPRIVATE=*.example.com,company.com/repo
  • 모든 모듈은 example.com을 하위 도메인으로 하는 위치의 저장소에 저장되어 있거나 company.com/repo로 시작하는 URL로부터 직접 다운로드 받을 수 있다.