메뉴

문서정보

Go 패키지관리

목차

Go 패키지관리

Go1.10 버전까지는 패키지를 관리하기 위한 마땅한 도구가 없었다. 하지만 프로젝트의 규모가 커지다 보면, 패키지 관리가 필요해지기 마련이라서 dep, glide, vgo 등의 다양한 툴들이 개발됐다. 요즘 같은 시대에 개발환경을 통일해야 하나라는 주장이 있기도 하지만, 소프트웨어 의존성을 관리의 경우 다양한 툴이 있다는 것은 좋은 소식이 아니다. 다행히 Go 1.11 버전 부터 Go modules로 통합이 됐다. 이제 개발자들은 어떤 종속성관리 툴을 사용해야할지 걱정할 필요 없이, 언어에서 제공하는 표준 툴로 개발할 수 있게 됐다. 참고로 내가 최근까지 사용하던 종속성 관리 툴은 glide 였다.

환경

(2019년 4월 15일 현재)내 개발 환경은 아래와 같다.

빠르게 사용해보기

$GOPATH가 아닌 다른 디렉토리에 프로젝트 디렉토리를 만들어보자.
$ mkdir /tmp/gotest/hello
$ cd /tmp/gotest/hello

hello 패키지 파일 hello.go를 만들었다.
package hello

func Hello() string {
    return "Hello, world."
}

패키지를 테스트하기 위한 테스트파일 hello_test.go를 만들었다.
package hello

import (
    "testing"
)

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

코드를 제대로 개발했는지 테스트해보자.
yundream@yundream:/tmp/gotest/hello$ go test
PASS
ok  	_/tmp/gotest/hello	0.002s

go mod init로 module을 초기화 한다.
$ go mod init github.com/yundream/hello
go: creating new go.mod: module github.com/yundream/hello
$ go test
PASS
ok  	github.com/yundream/hello	0.002s
이렇게 첫번째 모듈을 만들었다. go mod init 명령을 실행하면, 현재 패키지의 의존성 정보를 담고 있는 go.mod파일을 만든다.
$ cat go.mod 
module github.com/yundream/hello

go 1.12

의존성 추가하기

Go 모듈의 주요 목적은 다른 사람이 코드를 작성할 때의 경험(환경)을 그대로 공유하는데 있다.

hello.go는 인삿말을 출력한다. 나는 운영체제의 언어설정에 맞는 인사말을 출력하도록 프로그램을 개선하기로 했다.
package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}
내 운영체제의 언어설정은 ko_KR.UTF-8이므로 Hello() 함수는 "안녕, 세상."을 리턴할 것이다. 테스트를 통과하기 위해서 TestHello() 함수도 수정했다.
$ echo $LANG
ko_KR.UTF-8
$ go test
PASS
ok  	github.com/yundream/hello	0.003s

go 명령을 실행하면, 의존성을 확인해서 go.mod의 내용을 업데이트한다. go 명령을 실행하면 "rsc.io/quote"에 대한 의존성이 추가됐다는 것을 감지하고, go.mod에 의존성 정보를 추가한다. go.mod의 내용을 보자.
$ cat go.mod 
module github.com/yundream/hello

go 1.12

require rsc.io/quote v1.5.2
rsc.io/quote v1.5.2에 대한 의존성을 가지고 있음을 알 수 있다. 이렇게 go 에서 제공하는 명령만을 이용해서 빠르고 쉽게 새로운 의존성을 추가할 수 있다.

개발자는 go list -m all 명령으로 현재 패키지가 가지고 있는 모든 의존성 정보를 출력할 수 있다.
$ go list -m all
github.com/yundream/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
go 모듈은 go.mod 외에 go.sum 파일도 관리한다. 여기에는 특정 모듈의 버전에 해당되는 해시를 포함하고 있다. go 명령은 go.sum 파일의 정보를 이용해서 프로젝트가 의존하는 모듈이 악의적 혹은 우발적으로 변경되지 않도록 관리한다.

의존성 업데이트

Go 모듈은 semantic version tags로 버전을 관리한다. semantic version은 메이저, 마이너, 패치의 세 부분으로 구성이된다. 예를 들어 v0.1.2의 경우 주 버전은 0이고, 부 버전은 1, 패치버전은 2다. 이제 의존성 업데이트가 어떻게 관리되는지 살펴볼 것이다.

우리는 go list -m all 목록에서 untagged 버전의 golang.org/x/text을 사용하고 있다는 것을 확인했다. go get 명령으로 golang.org/x/text 를 최신버전으로 업그레이드했다.
$ go get golang.org/x/text
$ go test
PASS
ok  	github.com/yundream/hello	0.003s

go list -m all 과 go.mod 파일의 내용을 살펴보자.
$ go list -m all
github.com/yundream/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0

$ cat go.mod
module github.com/yundream/hello

go 1.12

require (
	golang.org/x/text v0.3.0 // indirect
	rsc.io/quote v1.5.2
)

golang.org/x/text 패키지의 마지막 버전(v0.3.0)으로 태깅된 걸 확인 할 수 있다. go.mod 역시 v0.3.0으로 업데이트 됐다. go.mod 내용을 보면 indirect 주석이 있는데, 직접적으로 사용하는 패키지가 아닌 간접적으로 사용하는 패키지라는 걸 알려준다.

이제 rsc.io/sampler을 업그레이드 해보자.
$ go get rsc.io/sampler
$ go test
--- FAIL: TestHello (0.00s)
    hello_test.go:10: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "안녕, 세상."
FAIL
exit status 1
FAIL	github.com/yundream/hello	0.002s
테스트가 실패했다. rsc.io/sampler 의 최신버전이 코드와 호환되지 않았다. rsc.io/sampler의 사용 가능한 버전을 확인해보자.
$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
v1.99.99는 지금은 좋은 선택이 아닌 것 같다. v1.3.1을 사용하기로 했다.
$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok  	github.com/yundream/hello	0.004s

새로운 메이저 버전 의존성 추가하기

hello 패키지에 Proverb()라는 새로운 함수를 추가했다. 이 함수는 rsc.io/quote/v3 모듈에서 제공하는 quote.Concurrency를 호출한다.
package hello

import (
    "rsc.io/quote"
    quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
     return quote.Hello()
}

func Proverb() string {
    return quoteV3.Concurrency()
}
hello_test.go 코드를 수정한다.
package hello

import (
    "testing"
)

func TestHello(t *testing.T) {
    want := "안녕, 세상."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

코드를 테스트해보자.
$ go test
PASS
ok  	github.com/yundream/hello	0.004s

새로운 의존성이 추가된 걸 확인 할 수 있다.
$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0

예제 프로젝트 만들어보기

bitbucket 을 기반으로 예제 프로젝트를 만들어보기로 했다. 소스코드는 "bitbucket.org/dream_yun/moduleapi"에 있다.
# cd $HOME/workspace
.
├── README.md
├── app
│   └── app.go
└── main.go
main.go 코드
package main // import "bitbucket.org/dream_yun/moduleapi"

import (
    "bitbucket.org/dream_yun/moduleapi/app"
) 

func main() {
    myapp := app.New("test app")
    myapp.Run()
}
app.go 코드
package app

import (
    "fmt"
    "github.com/gorilla/mux"   
    "net/http"
) 
  
type Application struct {
    router *mux.Router
}

func New(name string) *Application { 
    app := &Application{}
    app.router = mux.NewRouter()    
    return app
}

func (a *Application) Run() {
    a.router.HandleFunc("/user/hello", Handler_Hello)
    http.Handle("/", a.router)
    err := http.ListenAndServe(":8888", nil)
    if err != nil {
        fmt.Println(err)
    }
}

func Handler_Hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "hello world")    
}
모듈을 초기화하고 코드를 빌드한다.
# go mod init bitbucket.org/dream_yun/moduleapi
go: creating new go.mod: module bitbucket.org/dream_yun/moduleapi

# go build
go: extracting github.com/gorilla/mux v1.7.1
코드로 부터 gorilla/mux v1.7.1를 추출했다. go.mod 파일을 보자.
# cat go.mod 
module bitbucket.org/dream_yun/moduleapi

go 1.12

require github.com/gorilla/mux v1.7.1
코드로 부터 추출(extracting)한 패키지는 $HOME/go/pkg/mod 밑에 저장된다.
$ cd $HOME/go/pkg/mod/github.com/gorilla
$ ls
mux@v1.7.1
버전별로 패키지가 저장된 것을 확인 할 수 있다. gorilla/mux v1.7.0 버전을 사용하도록 go.mod 를 수정했다.
$ cat go.mod 
module bitbucket.org/dream_yun/moduleapi

go 1.12

require github.com/gorilla/mux v1.7.0
다시 빌드를 해보자.
$ go build
go: finding github.com/gorilla/mux v1.7.0
go: downloading github.com/gorilla/mux v1.7.0
go: extracting github.com/gorilla/mux v1.7.0
$ ls $HOME/go/pkg/mod/github.com/gorilla
mux@v1.7.0  mux@v1.7.1
logrus를 이용해서 애플리케이션 로그를 남기기로 했다. 아래와 같이 main.go 코드를 수정했다.
package main // import "bitbucket.org/dream_yun/moduleapi"

import (
    "bitbucket.org/dream_yun/moduleapi/app"
    log "github.com/sirupsen/logrus"
)   
  
func main() {
    log.WithFields(log.Fields{
        "Application": "TestApp",
        "Version:":    "v0.1.1",
    }).Info("Application start...")
    myapp := app.New("test app")
    myapp.Run()
}
$ go build
go: extracting github.com/sirupsen/logrus v1.4.1
$ cat go.mod 
module bitbucket.org/dream_yun/moduleapi

go 1.12

require (
	github.com/gorilla/mux v1.7.0
	github.com/sirupsen/logrus v1.4.1
)

예제 프로그램 배포

깨끗한 환경에서 테스트하기 위해서 golang docker이미지로 개발 테스트 환경을 만들었다.
# docker run -it --name modtest golang:1.12 /bin/bash
root@efaaf9824692:/go# go version
go version go1.12.4 linux/amd64
root@efaaf9824692:/go# mkdir ~/workspace 
root@efaaf9824692:/go# cd ~/workspace 
예제 코드를 clone 한다.
root@efaaf9824692:~/workspace# git clone git@bitbucket.org:dream_yun/moduleapi.git
root@efaaf9824692:~/workspace/moduleapi# go build
go: downloading github.com/sirupsen/logrus v1.4.1
go: downloading github.com/gorilla/mux v1.7.0
go: extracting github.com/sirupsen/logrus v1.4.1
go: extracting github.com/gorilla/mux v1.7.0
go: downloading golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
go: extracting golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
mux 버전을 보니, 원래 환경과 동일하게 빌드 한 것 같다. 패키지 정보를 살펴보기로 했다.
// 최초 개발 환경
$ go list -m all
bitbucket.org/dream_yun/moduleapi
github.com/davecgh/go-spew v1.1.1
github.com/gorilla/mux v1.7.0
github.com/konsorten/go-windows-terminal-sequences v1.0.1
github.com/pmezard/go-difflib v1.0.0
github.com/sirupsen/logrus v1.4.1
github.com/stretchr/objx v0.1.1
github.com/stretchr/testify v1.2.2
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33

// 도커 컨테이너 개발 환경
# go list -m all
bitbucket.org/dream_yun/moduleapi
github.com/davecgh/go-spew v1.1.1
github.com/gorilla/mux v1.7.0
github.com/konsorten/go-windows-terminal-sequences v1.0.1
github.com/pmezard/go-difflib v1.0.0
github.com/sirupsen/logrus v1.4.1
github.com/stretchr/objx v0.1.1
github.com/stretchr/testify v1.2.2
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
서로 다른 두 개의 개발환경을 동일하게 유지할 수 있다.