본문 바로가기
자습

Go 언어 초보의 Go modules 정리 노트

by litaro 2020. 12. 20.

이번엔 Go modules 에 대해 정리~ 맨땅에 헤딩하기는 어려우니 또 훌륭한분의 블로그를 따라가보자 ^^

medium.com/rungo/anatomy-of-modules-in-go-c8274d215c16

 

Anatomy of Modules in Go

Modules are a new way to manage dependencies of your project. Modules enable us to incorporate different versions of the same dependency…

medium.com

Go modules 이 안나온 시절은 어땠을까?

go get : 3rd party packages 가져오기

go에서는 'go get' 명령어로 외부 package를 가져올 수 있다.

Go는 Node.js의 NPM과 같은 중앙 package 저장소가 없다. 대신, (package 이름이 URL 형태인것을 보면 알 수 있듯이) internet 상의 어디에서나 가져올 수 있도록 했다.

예를 들어~

import "github.com/jordan-wright/email"

github.com/jordan-wright/email 에 가면 해당 package source code가 있다. 오 simple 한데~  

Go packages는 GIT, SVN 과 같은 VCS (version control system) 에 있고 package의 url로 접근해서 해당 VCS가 지원하는 protocol을 사용해서 다운받게된다. 이렇게 가져오는 방식이 simple하고 좋은데 문제는... 'go get'으로는 특정 branch나 tag, version 를 지정하지 못해서  가져올 수 없다는 것이다. 그 얘기는 항상 master repository의 최신 버전을 가져온다는 것이다. ^^;;

그렇다면 'go get'으로 가져온 package는 어디에 저장될까? Go packages 를 공부했으니 3rd party package라 해서 다른 것은 없다. 

모든 packages 는 $GOPATH/src 아래에 source file들이 저장되고, Go는 바로 compile 해서 $GOPATH/pkg 아래에 .a 파일을 생성한다. 그리고 'go build' 로 compile 하면 나의 main packages 는 $GOPATH/bin 에 실행파일로 생성되고 내부 참조 packages는 $GOPATH/pkg 에 .a 파일이 생성된다. 역시~ GOPATH 가 매우 중요하다....

무엇이 문제인가?

  • GOPATH를 안 바꾸려면 모든 project는 한 workspace 에 저장해야한다. 
  • 새로운 directory 만들어서 개발하려면 GOPATH를 바꾸고 다시 또 필요한 packages를 가져와야한다. (중복 코드... ^^;;)
  • package 가 저장되는 곳은 package name 과 같은 directory라서 여러 버전을 설치할 수 없다.  (만약 내가 사용하는 package가 새로운 기능을 추가하면서 기존 funciton을 바꾼다면 내 코드를 수정하지 않는 한 upgrade하면 안된다. 나의 새로운 프로젝트에서는 그 새로운 기능이 필요한데... 어쩔...)

 

이제 Go modules

기존 문제들을 해결하려면 Go modules 에서는 

  • GOPATH를 신경쓰지 않고 내가 원하는 곳에 sourcecode를 둘 수 있어야 한다.
  • 내가 필요로 하는 버전의 package를 install 할 수 있어야 한다.
  • 동일한 package의 여러 버전을 import 할 수 있어야 한다. (여러 프로젝트에서 서로 다른 버전의 package를 가져다 사용할 수 있다)
  •  NPM의 package.json 처럼 Go에서도 내가 사용하는 package list를 하나의 파일로 명시할 수 있어야한다. 그래야 코드 배포시 참조한 파일을 이 파일을 확인해서 GO가 알아서 설치할 테니까.

그리고 실제로 Go modules 은 이러한 기능을 제공한다.~

module은 package들의 집합으로 go.mod 파일과 이루어진다. (참조할 greetings 나 실행할 hello 모두 module) 

  • go.mod 파일에는 해당 module의 module path (import 할때 사용되는  name), 그리고 그 module이 build할때 의존하는 다른module 정보가 정의되어 있다.

 

 

말보다 한번 해봐야 느낌 오니까~

repository 생성

보통 module을 개발한다는 것은 배포하는 것이니까 선호하는 VCS 에 repository를 만들자

난 git에 repository를 하나 생성 : github.com/ryulitaro/greetings 이게 내 module이 import할때의 name이 되겠다.

module 생성

내가 원하는 경로에 module directory 'greetings'를 만들고

go mod init github.com/ryulitaro/greetings

그러면 greetings 밑에 go.mod 가 생긴다. 

블로그 처럼 multi packages로 module 만들어보려고 억지로 두개를 만들었다 ㅋㅋ 'multi'와 'single' 코드는 초보니까 golang.org/doc/tutorial/ 예제 코드를 사용해서 해보자~

다 되면 git 에 반영하고... 버전 관리가 Go modules 로 인해 어떻게 가능한지 보기 위해 versioning을 하자.

https://research.swtch.com/vgo-import

git add .
git commit -m "add multi/ single packages"
git push -u -f origin master
git tag v1.0.0
git push --tag

Test 할 main module 생성

이제 이 package를 import 할 hello 실행 파일을 만들기 위해 hello module을 만들자. (local 테스트용이니 module 이름을 그냥 hello)

go mod init hello

그리고 hello.go 에 import 하고 go run 하면 자동으로 git 에 올린 module 이 인식되어 실행된다. Hooray~~

hello % go run hello.go
go: finding module for package github.com/ryulitaro/greetings/single
go: found github.com/ryulitaro/greetings/single in github.com/ryulitaro/greetings v1.0.0
hello ==> main()

그런데 이렇게 clone 한 module code는 어디에 설치되는 것일까?

바로 $GOPATH/pkg/mod 밑에 저장된 것을 확인할 수 있다. 재밌는것은 version이 명시되어 저장된다. (아 그래서 version 별로 사용가능하구나...)

module 을 업데이트하면

대충 single.go의 'Hello' function의 parameter를 하나더 추가해서 git add, git commit 하고 v1.0.1 로 tagging 하자

greetings % git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 396 bytes | 396.00 KiB/s, done.
Total 4 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/ryulitaro/greetings.git
   aa1bcf0..6c0faec  master -> master
greetings % git tag v1.0.1
greetings % git push --tag
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/ryulitaro/greetings.git
 * [new tag]         v1.0.1 -> v1.0.1

그리고 hello module에서 새로운 Hello function을 쓰고 go run hello.go 하면 에러가 발생한다

이미 해당 module 있으면 GO는 go run 시 새로운 버전을 가져오지 않는다.

그래서 만약 새로운 버전을 쓰려면 manually update 해야한다.

방법은 

  • "go get -u" 현재의 major version의 최신 minor/patch version을 가져온다. 즉, v1.0.1 이라면 최신 v1.x.x 버전을 가져온다.
  • 특정 version을 가져오려면 'go get module@version"으로 명시적으로 가져올 수 있다.
hello % go get github.com/ryulitaro/greetings@v1.0.1
go: downloading github.com/ryulitaro/greetings v1.0.1

해당 version을 잘 가져온 것을 확인할 수 있다.

왜 'go get -u'는 major version의 최신 버전만 가져오는가?

Go에서는 version 관리 철학이 확실한것 같다. major version이 v1에서 v2로 바뀌었다는 것은 중요한 기능의 변경이고 동작도 다르게 할 것이기 때문에 다른 module로 간주한다.. 따라서 module import path부터 달라야 하고 사용자는 명시적으로 새로운 module을 설치하는것이 맞다는 것이다. 흠.. 내 개인적으로도 이해가 되고 공감 되는 정책인것 같다. 

Major Version을 업그레이드

v2.0.0으로 업그레이드 하려면 blog.golang.org/v2-go-modules에서 두가지 방법을 얘기한다.

  • v2+ 에 대해서 별도의 directory를 만들던가...(위 golagng 에서는 이 subdirectory strategy에 대해 예제를 보여준다.)

  • 아니면 별도의 v2+ branch를 생성하던가... (이 블로그에서는 이 방법에 대해 얘기한다.)

여기서는 두 번째 branch로 관리하는 방법을 살펴보기로 ~

새로운 v2 branch 만들고 새로운 변경은 여기에서 작업한다.

이때 중요한 것은 코드 변경 후 go.mod 파일의 module path에 'v2' prefix를 추가하는 것이다. Go는 추가한 'v2'의 의미를 이해하고  v2.x.x를 다운하게 된다. 그렇기에 vX 문법은 정확히 해야한다.

  • 관리를 용이하게 하기 위핸 branch를 'v2'로 만들고
  • go.mod 의 module path 에 /v2를 추가
  • git tag 로 version을 v2.x.x 로 하여 릴리즈하면 끝

(base) greetings % git branch
* master
(base) greetings % git checkout -b v2
Switched to a new branch 'v2'
(base) greetings % git branch
  master
* v2
(base) greetings % git commit -m "new release version v2.0.0"
[v2 c40decd] new release version v2.0.0
(base) greetings % git push -u origin v2
(base) greetings % git tag v2.0.0
(base) greetings % git push --tag
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/ryulitaro/greetings.git
 * [new tag]         v2.0.0 -> v2.0.0

v2 버전의 module 을 import 하고 go get

  • 호출하는 코드에서는 명시적으로 module 의 import path 를 /v2로 변경한다. (결국 하나의 코드에서 module path가 다르기에 두 major version 사용이 가능하다.)

  • install 은 변경된 Module path를 명시해서 go get 한다.
(base) hello % go get github.com/ryulitaro/greetings/v2
go: github.com/ryulitaro/greetings/v2 upgrade => v2.0.0

확인해보면 $GOPATH/pkg/mod 아래 잘 down 받아 있다. v1 버전과는 다르게 subdirecotry로 버전 폴더가 만들어진다. 아무래도 버전 관리를 위한 정책인것 같다.

실행파일을 만드려면 'go build'로 해당 경로에 만들거나 'go install'로 $GOPATH/bin/경로에 만들수 있다. 아래는 'go install'~

 

Minimal version Selection

major version 업데이트를 통해 v1.x.x 와 v2.x.x 같은 major version은 같은 여러 version을 같은 코드에 사용할 수 있는것을 알았다. 그렇다면 내가 minor 한 두 버전 v1.0.1 과 v1.0.2를 둘다 사용하려면? (module path가 같은데...)

결론은 함께 import하는 것은 불가능하고 결국 둘 중 최신 버전을 사용해야한다.

Go 에서는 minor version의 경우는 compatible 할 것을 권고한다. 당연히 v1.0.2는 v1.0.1의 기능을 다 가지고 있어야 한다는 것이다. 물론 틀린 말도 아니지만... 때때로 개발하다 보면 이 부분이 일정의 문제나 프로세스 문제로 특정 minor version을 유지해야하는 경우가 많다. Go module 이 아직 beta 상태이고 계속 업그레이드 될 수 있겠지만. 이 부분이 많은 개발자들이 꼽는 Go 의 단점중 하나인 것은 틀림없다.

Summary

  • Go module의 등장으로 기존에 GOPATH를 변경하면서 개발하던 불편함은 사라졌다.
  • Go module로 내가 원하는 version 을 사용할 수 있다.
  • 'go get -u' 는 자동으로 최신 module로 업데이트 해주는데 단, major version은 같아야 한다. (v1.0.1 이라면 v1.x.x 만...)
  • 새로운 Major version을 받으려면 module path에 해당 major version의 prfix vX가 추가되어야한다. greetings -> greetings/v2
  • Go module로 서로 다른 major version은 같이 사용할 수 있다. 하지만 minor version의 경우는 결국 둘 중 최신 version을 택해야한다.

한번 정리 해보니 Go module이 어떻게 동작하고 또 import 할때 무엇을 생각해야하는지에 대한 개념이 잡힌다. 주로 open되어 있는 유용한 module을 import해서 사용하는 사용자 입장일 것 같은데 어떻게 version 관리를 하면서 문제 없이 개발할지 큰 도움이 되었다.