go에서 라이브러리 관리는 저장소, 모듈 그리고 패키지라는 개념에 기반한다. 저장소는 프로젝트를 위한 소스 코드가 저장된 버전 관리 시스템이다. 가령, github의 repository같은 것들이 있다. 모듈은 저장소에 저장된 go라이브러리나 응용 프로그램의 최상위 루트이다. 모듈은 모듈 구성 및 구조를 제공하는 하나 이상의 패키지로 구성되어 있다.
즉 다음과 같이 생각할 수 있다.
- Github
- repo(모듈) -> package, package ...
- repo(모듈) -> package, ...
모든 모듈은 전역적으로 유일한 식별자를 가지고 있어야 한다. 가령, 자바에서는 com.companyname.projectname.library
와 같이 전역으로 유일한 패키지 이름을 사용한다.
go 소스 코드의 컬렉션은 해당 루트 디렉터리에 유효한 go.mod
파일이 있을 때 모듈이 된다. go.mod
파일은 해당 go프로젝트의 의존성(모듈)을 관리하는데, npm에서의 package.json
, spring에서의 pom.xml
과 비슷하다고 생각하면 된다. go mod init MODULE_PATH
을 통해서 go.mod
파일을 자동으로 생성할 수 있다. MODULE_PATH
은 앞서 이야기했던 go 모듈을 식별하는 유일한 식별자 역할을 한다. 주로, github에 저장소를 둔 경우에는 github.com/user/repo
로 이름을 짓는다.
가령, github에 저장소를 두고, learning-go-book
이라는 유저가 money
라는 github repo로 go 모듈을 만든다면 다음과 같이쓰면 된다.
go mod init github.com/learning-go-book/money
참고로 모듈 경로는 대소문자를 구분한다. 또한 혼선이 있기 때문에 대문자는 되도록 쓰지 않는다.
go.mod
파일이 만들어지고 다음과 같은 모습을 하게 된다.
module github.com/learning-go-book/money
go 1.15
replace(
...
)
require(
...
)
구성이 되는 요소들은 다음과 같다.
repository
패키지가 있다면 "github.com/learning-go-book/money/repository"
를 import
한다.require
에 있는 go모듈의 이름이나 버전을 replace할 수 있다. 특정 버전으로 변경하거나 특정 모듈의 이름이 달라졌을 때 유용하게 사용할 수 있다.go의 import
문은 다른 패키지에서 노출된(exposed) 상수, 변수, 함수 ,타입을 접근할 수 있도록 한다. 즉, 패키지의 노출된 식별자(변수, 상수, 타입, 함수, 메서드 혹은 구조체의 field)는 import
문 없이는 다른 패키지에서 접근할 수 없다. go에서 패키지 레벨 식별자를 외부에 보여주게 하기위해서는 이름 앞 글자를 대문자로 쓰기만 해주면 된다. 반대로 이름이 소문자로 시작하거나 _
로 시작하는 식별자는 선언된 패키지 내에서만 접근이 가능하다.
go의 패키지는 directory당 한 개만을 허용한다. 따라서 math
라는 디렉터리를 만들고 패키지를 만들어보도록 하자. go에서의 패키지 생성은 다음과 같이 한다.
package math
func Double(a int) int {
return a * 2
}
package
는 패키지 절(package clause)라고 하는데, package
키워드와 패키지 이름으로 구성된다. 패키지 절은 go 소스 파일에서 비어 있지 않고 주석이 아닌 첫번째 라인이다.
다음으로 formatter
디렉터리를 만들고 formatter.go
파일을 만들도록 하자.
package print
import "fmt"
func Format(num int) string {
return fmt.Sprintf("The number is %d", num)
}
formatter
디렉터리에 있지만 패키지 이름은 print
라고 지었다.
마지막으로 두 코드를 사용하는 main
의 코드이다.
package main
import (
"fmt"
"github.com/learning-go-book/package_example/formatter"
"github.com/learning-go-book/package_example/math"
)
func main() {
num := math.Double(2)
output := print.Format(num)
fmt.Println(output)
}
import
문을 보도록 하자. 표준 라이브러리를 제외한 두 개의 경로는 해당 프로그램에 있는 패키지를 참조한다. 이 사실로 알 수 있는 것은 표준 라이브러리를 제외한 모든 것들은 import path
를 반드시 지정해야한다는 것이다.
main
함수에서 다른 패키지에 있는 식별자(함수, 변수, 타입 등등)을 가져올 때는 package이름.식별자
로 호출한다.
디렉터리에 있는 모든 go파일은 동일한 패키지 절을 가져야 한다. 우리는 github.com/learning-go-book/package_example/formatter
경로의 모듈에서 print
패키지를 가여왔다. 이는 import
경로가 아니라 패키지 절로 패키지 이름으로 결정되기 때문이다. 때문에 formatter.Format
이 아닌 print.Format
을 호출하는 것이다.
일반적으로 디렉터리 이름과 패키지 이름을 일치하는 패키지 이름을 만드는 것이 좋다. 포함하는 디렉터리와 이름이 다르다면 패키지를 찾아내기 어려울 수 있다. 하지만 디렉터리와 패키지 이름을 다르게 사용해야 하는 몇 가지 상황들이 있다.
첫 번째는 특별한 패키지 이름인 main
을 사용하여 go 응용 프로그램의 시작점으로 패키지를 선언한다. main
패키지는 가져올 수 없기 때문에 혼란스러운 import
문을 생성하지 않는다.
두 번째 이유는 디렉터리를 사용하여 버전 관리를 지원하는 것이다. 이는 추후에 더 다뤄보도록 하자.
패키지 이름을 짓는 데에는 몇 가지 노하우들이 있다.
util
이라는 패키지 이름보다는 기능을 설명하는 패키지 이름이 더 좋다. util
에 숫자를 문자열로 바꿔주는 convertIntToString
함수와 이름을 포맷팅해주는 formatName
함수가 있다면, 둘 다 util.convertIntToString
, util.formatName
으로 사용해야한다. 여기에 util
은 그 어떠한 설명을 해주지 않는다. 차라리 두 개의 패키지로 나누어서 convert.IntToString()
과 format.Name()
으로 바꿔주는 것이 좋다.format.formatName
은 기능에 대한 설명을 두 번 반복하는 꼴이 된다. 다만 식별자의 이름이 패키지 이름과 같은 경우에는 해당 규칙에서 예외로 한다. 가령 sort
패키지에는 Sort
라는 함수가 있고, context
에는 Context
라는 인터페이스가 있다.때로는 중복된 이름의 두 패키지를 가져와야 하는 경우가 있다. 암호학적으로 안전한 cryto/rand
와 안전하지 않은 math/rand
가 있다. 이 둘 모두 같은 패키지 이름인 rand
를 가지는데, 함께 사용하는 일이 발생한다면 현재 파일 내에서 하나의 패키지에 대체 이름을 부여해야 한다.
package main
import (
crand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
)
func seedRand() *rand.Rand {
var b [8]byte
_, err := crand.Read(b[:])
if err != nil {
panic("cannot seed with crytographic random number generator")
}
fmt.Println(b) // [230 99 80 178 36 21 38 57]
r := rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(b[:]))))
return r
}
func main() {
seedRand()
}
위의 코드는 crypto/rand
에서 랜덤한 8개의 숫자를 가져오고 이를 math/rand
의 New
매서드에 seed로 넣어 새로운 랜덤 값을 생성하는 코드이다. crypto/rand
와 math/rand
는 패키지 이름이 동일하므로 충돌이 발생한다. 따라서, "crypto/rand"
에 대체 패키지 이름으로 crand
를 부여한 것이다. 이렇게 함으로서 crypto/rand
의 식별자와 math/rand
의 식별자가 충돌하지 않은 것이다.
crand.Read(b[:])
다음과 같이 접근한 것을 확인할 수 있다.
go는 자동적으로 문서로 변환해주는 주석을 작성하기 위한 go
만의 포맷을 가진다. 이를 godoc
이라고 부른다. godoc
주석에는 특별한 심볼은 없고 관례만 있다.
패키지 선언 전에 주석은 패키지 레벨 주석을 생성한다. 패키지를 위한 긴 주석을 작성해야한다면, 따로 패키지 내에 doc.go
라는 파일에 주석을 넣는 것이 관례이다.
// package money provides various utilities to make it easy to manage money
package money
다음으로 노출된 구조체에 대한 주석 작성이다.
//Money represents the combination of an amount of money
// and the currency the money is in.
type Money struct {
Value decimal.Decimal
Currency string
}
함수의 주석 예이다.
// Convert converts the value of one currency to another
//
// It has two parameters: a Money instance with the value to convert,
// and a string that represents the currency to convert to. Convert returns
// the converted currency and any errors encountered from unknown or unconvertible
// currencies
// If an error is returned, the Money istance is set to the zero value.
//
// Supported currencies are:
// USD - US Dollar
// CAT - Canadian Dollar
//
// More information on exchange rates can be found
// at https://wwww.investopedia.com/terms/e/excahngerate.asp
func Convert(from Money, to string) (Money, error) {
// ...
}
godoc을 만들기 위해서는 go doc
커맨드를 사용하면 된다. go doc PACKAGE_NAME
은 지정된 패키지를 위한 godoc
과 패키지의 식별자 목록을 보여준다. 패키지의 특정 식별자를 위한 문서를 출력하기 위해서는 go doc PACKAGE_NAME.IDENTIFIER_NAME
명령어를 사용하도록 하자.
최소한 패키지에서 exposed 코드에 대한 주석이 있어야 한다.
때로는 모듈에서 패키지 간에 함수, 타입, 상수를 공유하고 싶지만 이를 API
의 일부가 되게 하고 싶지 않은 경우가 있다. 가령 사용자에게 database
기능을 제공하는 database
모듈을 라이브러리로 만들었는데, 이 내부에서 공용으로 사용하는 식별자들에 대해서는 사용자에게 노출시키고 싶지 않을 것이다. 대문자로 시작하는 식별자를 최대한 쓰지 않으면 되지만, 해당 라이브러리 내의 패키지끼리의 로직을 위해서는 불필요하게 대문자로 시작하는 식별자를 만들 수 밖에 없는 경우가 있다. 이렇게 되면 해당 라이브러리를 사용하는 사용자도 해당 패키지 내부에서만 통용으로 쓰려고 만든 패키지의 식별자들에 대해 접근할 수 있어 문제가 된다. 이를 위해서 go에서는 특별한 패키지인 internal
이라는 패키지를 제공한다.
즉 이제 database
라는 모듈은 내부 패키지들 끼리 공용할 식별자들을 internal
에 넣어주고 호출하면 된다. 사용자는 database
모듈을 사용한다해도 internal
에는 접근이 불가능하여 대문자로 시작하는 식별자에 접근이 안된다.
한 가지 조심할 것은 해당 모듈에서도 internal
의 접근이 일부 제한되는데, internal
패키지는 자신과 같은 레벨에 있거나, 바로 상위의 부모 패키지 외에는 자신을 호출하지 못한다. 다음의 구조를 확인해보자.
* bar
- bar.go
* foo
- foo.go
* internal
- internal.go
* sibling
- sibling.go
foo
패키지와 sibling
패키지는 internal
패키지와 같은 위치에 있거나 바로 상위에 있는 위치이다. 그러나 bar
는 바로 상위도 아니고, 같은 위치에 있지도 않으므로 bar
패키지에서는 internal
패키지에 접근할 수 없다.
어떠한 파라미터도 받지 않고 반환하는 값이 없는 init
이라는 함수를 선언하면 다른 패키지에서 패키지가 처음 참조되는 순간 수행된다. init
함수는 어떠한 입력과 출력을 가지지 않기 때문에, 패키지 레벨 함수나 변수와 상호 작용하는 범위에서만 동작한다. 즉, init
함수는 패키지가 초기에 호출될 때 상태를 설정하는 부분인 것이다.
재밌는 것은 특정 패키지들은 init
만 호출되고 어떠한 식별자(변수, 함수, 구조체, 인터페이스 등)이 참조되지 않는 경우가 있다. 단, golang의 경우 어떠한 식별자도 호출되지 않으면 해당 패키지를 import`하는 것을 막기 때문에 다음과 같은 패턴을 사용해야한다.
import (
"database/sql"
_ "github.com/lib/pq"
)
github.com/lib/pq
패키지에 있는 어떠한 식별자를 쓰지 않고 init
만 호출해야한다면 위와 같이 _
로 공백 가져오기(black import
)를 하면된다. 이렇게 해도 import
할 때 init
이 호출될 것이다.
이러한 패턴을 registy pattern
이라고 하는데, go의 표준 라이브러리인 database/sql
에 데이터베이스 드라이버를 init
으로 심어주는 것이다. 그러나, 이러한 코드는 이제 사장되었다고 보면된다. 코드가 명시적이지 않고 디버깅을 어렵기 하기 때문이다. 때문에 현재는 명시적으로 플러그인을 등록하면 된다.
init
함수의 주요 사용처는 단일 할당에서 설정될 수 없는 패키지 레벨 변수를 초기화하는 경우이다. 그러나 사실 패키지 레벨 변수를 관리하는 것 역시도 좋은 방법이 아니기 때문에 init
의 사용은 긍정적인 신호가 아니다.
go는 third-party의 패키지를 통합하기 위해서도 같은 표준 라이브러리와 같은 import
방식을 사용한다. 다른 컴파일 언어와 달리 go는 응용 프로그램을 위한 third-party에서 가져온 코드와 본인이 작성한 코드 모두를 컴파일하여 단일 바이너리로 만든다. 프로젝트 내에 있는 패키지를 가져올 때처럼, third-pary 패키지를 가져올 때는 패키지가 있는 소스 코드 저장소의 위치를 지정하기만 하면된다.
package main
import (
"fmt"
"log"
"os"
"github.com/learning-go-book/formatter"
"github.com/shopspring/decimal"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Need two parameters: amount and percent")
os.Exit(1)
}
amount, err := decimal.NewFromString(os.Args[1])
if err != nil {
log.Fatal(err)
}
percent, err := decimal.NewFromString(os.Args[2])
if err != nil {
log.Fatal(err)
}
percent = percent.Div(decimal.NewFromInt(100))
total := amount.Add(amount.Mul(percent)).Round(2)
fmt.Println(formatter.Space(80, os.Args[1], os.Args[2], total.StringFixed(2)))
}
가져온 두 개의 모듈 github.com/learning-go-book/formatter
와 github.com/shopstring/decimal
은 third-party 모듈을 지정한 것이다. 해당 모듈은 저장소의 위치로 포함시킨다는 것을 기억하도록 하자. 이들을 가져오기 위해서는 go get <pakcage storage location>
을 사용하면 된다.
go get github.com/learning-go-book/formatter
go get github.com/shopspring/decimal
일단 가져오면, 다른 가져온 패키지들과 같이 해당 패키지의 노출된 항목을 접근 할 수 있다. go.mod
에 가보면 다음과 같이 되어있을 것이다.
module github/gyu-young-park/learning-go
go 1.19
require (
github.com/learning-go-book/formatter v0.0.0-20200921021027-5abc380940ae // indirect
github.com/shopspring/decimal v1.3.1 // indirect
)
go.mod
파일의 require
섹션에서 자신이 가져온 모듈을 기록한다. 모듈 이름 다음이 버전의 이름이다. formatter
의 경우는 버전 태그를 가지고 있지 않아 go가 pseudo version을 만들어 기록한 것이다. go.sum
도 설치한 모듈들이 기록되었을 것이다. go.sum
파일은 2개의 항목이 업데이터 되었을 텐데, 하나는 모듈과 모듈의 버전 그리고 모듈의 해시이다. 다른 하나는 모듈을 위한 go.mod
파일의 해시이다. 이러한 해시값들이 어디에 쓰이는 지는 추후에 알아보자.
의존성이 필요한 go
명령(go run
, go build
, go test
, go list
)를 실행하면 go.mod
에 아직 기록되지 않은 임포트 모듈이 캐시에 다운로드된다. go.mod
파일은 모듈의 버전과 패키지를 포함하는 모듈 경로를 포함하면 자동으로 업데이트 된다.
기본적으로 go get
으로 모듈을 설치할 때 버전을 따로 명시해주지 않으면 최신버전을 가져온다. 어떤 버전들이 있는 지 궁금하다면 go list
명령어로 해당 모듈의 버전들을 알아낼 수 있다.
go list -m -versions github.com/learning-go-book/simpletax
github.com/learning-go-book/simpletax v1.0.0 v1.1.0
v1.0.0
과 v1.1.0
이 있는 것을 확인하였다. go list
명령어는 프로젝트에 사용된 패키지들을 나열한다. -m
플래그는 패키지 대신에 모듈을 나열하도록 변경하고, -version
플래그는 go list
가 지정된 모듈의 가능한 버전을 보고하도록 변경한다.
만약, 낮은 버전을 선택하고 싶다면 다음과 같이 go get
명령어를 수행하면 된다.
go get github.com/learning-go-book/simpletax@v1.0.0
go get
명령어는 모듈과 관련된 작업과 의존성 업데이트를 할 수 있도록 한다.
참고로 go.mod
파일에서 // indirect
라는 레이블이 붙은 의존성을 볼 수 있다. 이것은 프로젝트에 직접적으로 선언되지 않은 의존성이다. go.mod
파일에 추가되지 않은 몇 가지 이유가 있는데, 한 가지 이유는 프로젝트가 go.mod
파일이 없는 이전 모듈에 의존하거나, go.mod
파일에 오류가 있고 일부 의존성이 누락되어있기 때문이다. 모듈을 빌드할 때 모든 의존성은 반드시 go.mod
파일에 나열되어야 한다. 의존성 선언이 어단 가에 있어야 하기 때문에 go.mod
가 수정되는 것이다.
직접 의존성이 간접 의존성을 알맞게 지정하지만 프로젝트에 설치된 것보다 이전 버전을 지정하는 경우 간접 선언이 있을 수도 있다. go get
으로 간접 의존성을 명시적으로 업데이트하거나 의존성 버전을 낮출 때 발생한다.
go 모듈은 시멘틱 버전 관리를 따른다. SemVer
은 major
, minor
, patch
로 나뉘며 v
문자를 필두로 major.minor.patch
로 표기한다. 패치 버전은 버그 수정이 있을 때 증가하고, minor
는 기존 버전과 호환을 유지하면서 새로운 기능이 추가될 때 증가한다. 이 때 패치는 0부터 다시시작한다. major
는 기존 버전과 호환되지 않은 변경이 있을 때 증가하고, minor
와 patch
는 0부터 시작한다.
만약 프로젝트에 추가한 third-party 모듈 2개가 동일한 모듈을 의존하지만 다른 버전을 의존한다면 어떤 버전이 설치될까?? go모듈은 최소 버전 선택 원칙(minimum version selection)을 사용한다. 이는 go.mod
에 선언된 의존성들을 만족하는 가장 낮은 버전의 공통 모듈을 불러온다는 것이다. go는 모듈의 requirement를 만족하는 가장 최소 버전을 선택하여 종속성 트리를 만들어 버전 충돌을 막고, 불필요한 모듈을 포함하지 않는다. 단, 오해하면 안되는 것이 최소라는 말이 가장 낮은 버전을 선택한다는 것이 아니다. 최소한 모듈들이 안정적으로 돌아갈 모듈을 선택한다는 것이다.
가령, main application이 있고, 모듈 A, B, C를 사용하고 있다고 하자. 이들이 모듈 D를 의존한다고 하고, 모듈 A는 모듈 D의 v1.1.0을 의존하고, 모듈 B는 모듈 D의 v1.2.0에 의존하고, 모듈 C는 모듈 D의 v1.3.0에 의존한다면 minimum version selection은 어떻게 되는가? 이때 minimum version selection
에 의해 v1.3.0
이 선택된다. 즉, v1.3.0
이 v1.1.0
, v1.2.0
, v1.3.0
을 모두 만족하는 최소한의 버전이라는 것이다. 아까도 말했듯이 최소는 가장 낮은 버전이 아니라, 최소한 동작하는 데 무리가 없는 버전을 말하는 것이다. v1.1.0
을 선택하면 모듈 B,C에 문제가 발생할 수 있고, v.1.2.0
를 선택하면 모듈 C에 문제가 발생할 수 있기 때문에, 이들 중 가장 높은 버전인 v.1.3.0
을 선택한 것이다. 그런데 최소라고 말한 것은, 모듈 D가 v1.7.0
과 같이 높은 버전을 가지고 있어도 이를 선택하지 않는다는 것이다. 이는 불필요한 라이브러리를 가져올 수 있기도하고, minor
와 patch
가 너무 달라져 하위호환성이 성립하지 않을 수 있기 때문이다.
golang의 convention은 major
가 달라지지 않는 이상 무조건 minor
와 patch
는 하위호환성을 맞춰야한다. 만약 모듈 D를 v1.3.0
으로 선택했는데, 모듈 A, B가 모듈 D를 사용하는데 있어 호환하지 못하면 이는 개발자에게 당당하게 찾아가 잘못만들었으니 고치라고 해야한다.
npm
과 같은 빌드 시스템에서는 같은 패키지에 여러 버전을 포함한다. 그렇기 때문에 npm
모듈의 크기가 굉장히 비대해지고, 심한 경우는 버저닝 떄문에 버그가 발생할 때가 있는 것이다.
https://go.dev/ref/mod#minimal-version-selection
재밌는 것은 golang에서 하위호환성을 맞추지 못하면 major
를 올려야하는데, semantic import version관리는 다음의 규칙을 따른다.
1. 모듈의 major 버전은 반드시 증가해야한다.
2. 0과 1을 제외한 major버전의 경우는 경로 마지막에 vN
으로 끝나야한다. N
은 major버전 값이다.
가령, github.com/learning-go-book/simpletax
의 v1.1.0
모듈은 github.com/learning-go-book/simpletax
으로 import하면 된다.
그런데, 만약 major가 2로 올라가서 v2.0.0
이 되었다면 github.com/learning-go-book/simpletax/v2
로 모듈을 바꾸어야 주어야 한다.
다음의 github 예제를 살펴보면 더 와닿는다.
https://github.com/learning-go-book/simpletax
기존의 코드를 두고 v2
디렉터리가 따로 있는 것을 확인할 수 있다. 따라서 go get
도 v2
를 기준으로 해주어야 한다.
go get github.com/learning-go-book/simpletax/v2
모듈이 항상 동일한 의존성으로 빌드되는 것을 보장하기 위해 어떤 조직에서는 해당 모듈 내에 의존성의 복사본을 유지하고 싶어한다. 가령 npm
의 경우 node_modules
를 매번 삭제하여 새로 생성해주는 것이 아니라 패키징 시에 그대로 유지하도록 하는 것이다. go에서는 이를 vendoring이라고 한다. go mod vendor
명령을 수행하여 활성화 시킬 수 있다. 이것은 모듈의 최상위 디렉터리에 모듈이 가지는 의존성 모두를 포함하는 verndor
라는 디렉터리를 생성한다.
go mod vendor
vendor
디렉터리에 우리가 사용했던 모듈들이 설치된 것을 확인할 수 있다. 단, go.mod
에 새로운 의존성을 추가하거나 버전을 변경하면 vendor
디렉터리에 새로 업데이트해주기 위해 go mod vendor
를 잊지말도록 하자. 만약 버전이 안맞거나, 모듈이 없으면 go build, go run, go test
에서 에러가 발생하니 조심해야한다.
의존성을 벤더링할 지는 조직에 달려있다. 예전에는 별다른 수없이 go mod vendor
를 사용했지만, go 모듈과 프록시 서버의 등장으로 이러한 방식은 선호되지 않는다. 즉, B2B
로 회사에 코드를 제공해야하는 입장에서도 go mod vendor
는 이제 사장된 방식이다.
라이브러리를 위한 단일의 중앙 집중 저장소에 의존하는 대신에 go는 하이브리드 모델을 사용한다. 모든 go 모듈은 github와 gitlab과 같은 소스코드 저장소에 저장된다. 하지만 기본적으로 go get
은 소스 코드 저장소에서 직접 가져오지는 못한다. 대신에 해당 명령은 구글에서 수행하는 프록시 서버로 요청한다. 해당 서버는 거의 모든 공개 go 모듈의 모든 버전을 복사하여 보관한다. 모듈이나 모듈의 버전이 프록시 서버에 없다면 모듈 저장소에서 다운로드를 받아 복사본을 저장하고 모듈에게 반환한다.
프록시 서버 외에도 구글은 sum database
도 유지 관리한다. 모든 모듈의 모든 버전의 정보를 저장하고 있는 것이다. 여기에는 go.sum
파일에 보여지는 해당 모듈의 버전과 모듈에 대한 체크섬을 포함한다.
프록시 서버가 인터넷에서 제거되는 모듈이나 모듈의 버전으로부터 사용자를 보호하는 것과 같이 sum database는 모듈의 버전 수정으로부터 사용자를 보호한다. 이것은 악의적일 누군가가 모듈을 가로채 악의적인 코드를 넣는 경우일 수도 있고, 부주의하여 버그를 수정하거나 새로운 기능을 추가해서 기존 버전 태그를 재사용하는 경우도 있다. 이런 두 경우에 같은 바이너리로 빌드하지 못하거나 응용 프로그램에 어떤 영향이 있는 지 알 수 없기 때문에 변경된 모듈 버전 사용을 원치 않을 것이다.
매번 go build
, go test
, go get
을 이용해 모듈을 다운로드 받을 때마다 go 도구는 모듈을 위한 해시를 계산하고 sum database에 접근하여 모듈 버전을 위한 저장된 해시와 계산된 해시를 비교한다. 만약 두 해시 값이 일치하지 않으면 모듈은 설치되지 않는다.
third-party 라이브러리의 요청을 구글에 보내는 것을 원치 않을 경우가 있다. 이를 위한 몇가지 옵션이 있다.
GOPROXY
환경 변수의 값을 direct
로 설정하여 프록시 서버를 완전히 비활성화 할 수 있다. 해당 저장소에서 직접 모듈을 다운받을 수 있지만, 저장소에서 지워진 버전에 의존하고 있다면 더 이상 접근할 수 없다.Arifactory
와 Sonatype
은 해당 제품의 엔터프라이즈 저장소 제품에 go
프록시 서버를 포함한다. Athens
는 오픈 소스 프록시 서버를 제공한다. 이런 제품 중 하나를 네트워크 내에 설치하여 GOPROXY
에 해당 URL
로 가리키게 하면 된다.GOPROXY 환경 변수를 사용하여 Go 모듈을 다운로드할 때 비공개 저장소를 지정하는 방법은 다음과 같다.
비공개 저장소 구성
Go 모듈을 호스팅할 비공개 저장소를 구성한다. 가령 gitlab
, github.com
엔터프라이즈 등이 있다. 비공개 저장소와 함께 직접 프록시를 구성하여 모듈을 관리하고, 보안상의 이점을 얻을 수 있다. 이를 위해서는 내부적으로 Nexus, Artifactory 또는 Goproxy.io와 같은 유명한 프록시 서버를 사용할 수 있다.
저장소에 인증 구성
비공개 저장소를 사용하기 위해서는 해당 저장소에 대한 인증 구성이 필요하다. 저장소에서 제공하는 인증 방법에 따라 달라질 수 있으며, 대개는 HTTP Basic Authentication, OAuth 또는 API 키를 사용합니다.
GOPROXY 설정
GOPROXY 환경 변수에 비공개 저장소의 URL을 설정한다. 예를 들어, 내부적으로 실행 중인 Nexus 프록시 서버를 사용하려면 다음과 같이 GOPROXY를 설정할 수 있다.
export GOPROXY=http://nexus.example.com/repository/go-proxy/
GOPRIVATE
환경 변수에 저장소의 호스트 이름을 추가하고, GONOPROXY
환경 변수에 저장소의 패턴을 추가하여 프록시 서버에 인증 정보를 제공합니다.export GOPRIVATE=example.com
export GONOPROXY=*.example.com
이제 Go 모듈을 다운로드하기위해 go get 명령어를 실행할 때 GOPROXY
환경 변수가 설정되어 있으므로, Go는 해당 저장소에서 모듈을 다운로드하려고 시도한다. 만약 인증 정보가 필요한 경우, GOPRIVATE
및 GONOPROXY
환경 변수를 사용하여 인증 정보를 제공한다.
즉, GOPRIVATE
는 인증 작업이 필요한 비공개 저장소를 의미한다. 만약 example.com
이 인증이 필요하다면 GORPIVATE=example.com
을 쓰면된다. 이 경우 example.com
인증을 위한 token을 제시하라고 할 것이다. 만약, GOPRIVATE
에 해당 호스트를 써주지 않으면 GOPROXY
서버는 아무리 회사의 네트워크 망에서 배포된 프록시 서버라도 비공개 저장소에 접근할 인증이 안되기 때문에 접근이 불가능하다. github
인증에 필요한 정보를 적어두어도 GOPRIVATE
에 인증이 필요한 호스트 이름을 써주지 않으면 GOPROXY
에서 인증 작업이 이뤄지지 않는다.
재미난 것은 GONOPROXY
에 GOPRIVATE
에 있는 호스트 이름을 똑같이 적어도 GOPROXY
가 적용된다. 이는 GOPRIVATE
에 있는 호스트는 비공개 저장소이기 때문에, 보안상의 이유로 인증이 필요하며 GOPROXY
를 통해 안전하게 모듈을 설치해야하기 때문이다.
따라서, 위의 예제를 보면 GOPRIVATE
는 example.com
인 반면에 GONOPROXY
는 *.example.com
이다. 이는 example.com
비공개 저장소에 대해서는 인증이 필요하지만, 이 아래의 sub.example.com
, sub2.example.com
이라는 서브 도메인에 대해서는 GONOPROXY
패턴이 적용되어 GOPROXY
를 거지치 않고 다운로드 할 수 있다는 것이다.
비공개 저장소에 있는 모듈을 GONOPROXY
에 설정은 사실 선택적인 부분이다. 비공개 저장소는 인증을 필요로 하기 때문에 GOPROXY
에서 저장소로 인식하지 못한다. 따라서, GOPRIVATE
를 설정해주어 비공개 저장소에 대한 인증을 마무리 하면, GONOPROXY
로 설정하지 않아도 해당 비공개 저장소의 모듈들을 가져올 수 있다.
그러나, GONOPROXY
를 설정해주지 않으면 GOPRIVATE
하위의 도메인일지라도 GOPROXY
를 통해서도 모듈을 요청한다. 즉, GOPROXY
가 비공개 저장소와 저장된 공개 저장소에서 둘 다 모듈을 찾으려는 시도가 이루어진다는 말이다. 이를 악용하여 해커가 원치 않은 모듈을 설치하게 만들 수 있다. 따라서, 비공개 저장소의 모듈에 대해서는 GONOPROXY
로 설정해주는 것이 좋다.
참고로 직접 구성한 프록시 서버에 비공개 저장소 모듈을 모두 넣어서 GOPRIAVTE
와 GONOPROXY
없이 모듈 요청을 처리할 수 있다. 그러나 이러한 방법은 보안상의 위배되는 점이 많아 추천되지 않는다.
대부분의 조직은 조직의 코드를 비공개 저장소에 보관한다. go프로젝트 내에 비공개 모듈 사용을 원한다면 구글 프록시 서버로 해당 모듈을 요청할 수는 없다. go는 비공개 저장소를 직접 확인하는 것으로 대체되지만, 비공개 서버 및 저장소의 이름을 외부 서비스로 유출하고 싶지 않을 수 있다.
자체 프록시 서버를 사용하거나 프록시를 비활성화했다면 문제가 되지 않는다. 비공개 프록시 서버를 운영하는 것은 몇 가지 추가적인 이점이 있다. 먼저 회사의 네트워크에 캐시되어 있기 때문에 third-party 모듈의 다운로드 속도를 높일 수 있다. 비공개 저장소에 접근하는 것이 인증을 요구한다면 프록시 서버의 사용은 CI/CD 파이프라인에서 인증 정보를 노출시키는 것에 관하여 걱정할 필요가 없다는 의미이다. 비공개 프록시 서버는 비공개 저장소에 인증하기 위해 설정이 되지만 비공개 프록시 서버에 대한 호출은 인증할 필요가 없다.
공개 프록시 서버를 사용한다면 GOPRIVATE
환경 변수에 쉼표로 구분된 비공개 저장소의 목록을 설정할 수 있다. 가령 .example.com
과 company.com/repo
아래에 있는 비공개 저장소에 있는 모듈에 접근하고 싶다면 다음과 같이 쓰면 된다.
GOPRIVATE=*.example.com,compancy.com/repo
모든 모듈은 example.com
을 하위 도메인으로 하는 위치의 저장소에 저장되어 있거나, company.com/repo
로 시작하는 URL로 부터 직접 다운로드 받을 수 있다. 그러나 이는 proxy서버를 거치지 않는다는 말이 아니다. 보통의 비공개 저장소는 인증이 필요하고 프록시 서버에서는 인증이 불가능하다. 따라서 GOPRIVATE
로 설정해놓은 비공개 저장소에 있는 모듈에 대해서는 GOPROXY
서버와 함께 설정된 인증 정보를 사용하여 다운로드를 한다.