모놀리식 작업 공간에서 모듈식 명확성으로: Go의 종속성 관리 진화 이해
Min-jun Kim
Dev Intern · Leapcell

Go는 단순성, 동시성 및 강력한 표준 라이브러리로 유명하며, 처음에는 GOPATH
를 통해 프로젝트 구조 및 종속성 관리에 대한 고유한 접근 방식을 제시했습니다. 간단한 프로젝트에는 간단했지만, 이 모델은 언어가 성숙하고 프로젝트가 복잡해짐에 따라 곧 한계를 드러냈습니다. GOPATH
에서 Go Modules로의 진화는 버전 관리, 재현성 및 격리와 같은 중요한 문제를 해결하여 현대 소프트웨어 개발을 위한 강력한 도구로서의 입지를 확고히 함으로써 Go 개발의 중요한 이정표를 나타냅니다.
GOPATH
시대: 중앙 집중식 및 단순 (Go 1.0 - 1.10)
Go Modules 이전에는 GOPATH
가 Go 개발의 초석이었습니다. 모든 Go 소스 코드, 컴파일된 패키지 및 실행 가능한 바이너리가 상주하는 단일 작업 공간을 정의했습니다.
GOPATH
의 구조 이해
GOPATH
는 디렉터리를 가리키는 환경 변수였으며, 일반적으로 기본적으로 $HOME/go
였습니다. 이 디렉토리 내에서 Go는 특정 구조를 기대했습니다.
src/
: 모든 소스 파일을 포함하며, 해당 가져오기 경로별로 구성됩니다. 예를 들어github.com/user/project
는$GOPATH/src/github.com/user/project
에 있습니다.pkg/
: 더 빠른 빌드를 위해 컴파일된 패키지 객체 (예:.a
파일)를 저장했습니다.bin/
: 컴파일된 실행 가능 프로그램을 보관했습니다.
GOPATH
하의 워크플로
go get github.com/some/package
를 사용하면 Go 도구 체인은 패키지의 소스 코드를 $GOPATH/src/github.com/some/package
에 직접 다운로드합니다. 모든 프로젝트는 개별 요구 사항에 관계없이 이 단일 다운로드된 버전의 종속성을 사용합니다. 자신의 프로젝트 소스 코드도 go
도구로 검색하고 빌드할 수 있도록 $GOPATH/src/
내에 있어야 했습니다.
간단한 GOPATH
시대 프로젝트 구조로 설명해 보겠습니다.
$GOPATH
└── src
└── github.com
└── myuser
└── myproject
└── main.go
└── some_dependency
└── some_dependency.go # go get을 통해 다운로드
인기 있는 웹 프레임워크인 github.com/gin-gonic/gin
을 사용하는 main.go
를 고려해 보겠습니다.
// $GOPATH/src/github.com/myuser/myproject/main.go package main import ( "log" "net/http" "github.com/gin-gonic/gin" // $GOPATH/src에서 암시적으로 찾습니다. ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) log.Println("Server starting on :8080") r.Run(":8080") // 0.0.0.0:8080에서 수신 대기 및 서비스합니다. }
이를 빌드하고 실행하려면 $GOPATH/src/github.com/myuser/myproject/
로 이동하여 go build
또는 go run .
을 실행합니다.
GOPATH
의 한계
단순하지만 GOPATH
는 프로젝트 복잡성이 증가함에 따라 여러 가지 중요한 단점을 겪었습니다.
- 버전 관리 없음: 모든 프로젝트는
GOPATH
에 설치된 종속성의 정확히 동일한 버전을 공유했습니다. 프로젝트 A에package@1.0.0
이 필요하고 프로젝트 B에package@2.0.0
이 필요한 경우,GOPATH/src
에 하나의 버전만 존재할 수 있으므로 "종속성 지옥"으로 이어졌습니다. 이로 인해 재현 가능한 빌드가 매우 어려워졌습니다. - 격리 부족: 프로젝트는 서로 격리되지 않았습니다. 한 프로젝트의 종속성 변경으로 인해 동일한 (전역)
GOPATH
설치에 의존하는 다른 프로젝트가 실수로 손상될 수 있습니다. - 프로젝트 위치 제약 조건: 프로젝트 소스 코드는
GOPATH/src/
내에 있어야 했으므로 많은 개발자에게 제한적이고 부자연스럽게 느껴졌습니다. 시스템의 어느 곳에서나 리포지토리를 복제하고GOPATH
를 변경하지 않고도go build
가 작동할 것으로 예상할 수 없습니다. - 느린 빌드:
pkg/
가 도움이 되었지만 강력한 종속성 캐싱이 부족하고 빈번한go get
작업이 필요하여 개발 속도가 느려질 수 있습니다.
이러한 제한으로 인해 Go 커뮤니티는 공식 솔루션이 등장하기 전에 dep
및 Glide
와 같은 다양한 비공식 종속성 관리 도구를 찾게 되었습니다.
Go Modules: 현대적이고 강력한 솔루션 (Go 1.11 이상)
Go 1.11에 도입되어 Go 1.13에서 기본값이 된 Go Modules는 버전 관리, 격리 및 재현 가능한 빌드에 대한 기본 제공, 최고 수준의 지원을 제공하여 종속성 관리에 혁명을 일으켰습니다.
Go Modules의 핵심 개념
Go Modules는 프로젝트가 프로젝트 루트 디렉터리 내에서 종속성 및 특정 버전을 직접 선언할 수 있도록 하여 GOPATH
의 단점을 해결합니다.
-
모듈 정의 (
go.mod
): Go 모듈의 핵심은go.mod
파일입니다. 이 파일은 모듈의 경로 (ID), Go 버전 요구 사항 및 해당 최소 필수 버전이 있는 직접 및 간접 종속성 목록을 정의합니다.module example.com/my-app
go 1.22
require ( github.com/gorilla/mux v1.8.0 rsc.io/quote v1.5.2 // rsc.io/sampler의 전이적 종속성 ) ```
-
무결성을 위한 체크섬 (
go.sum
):go.sum
파일은 모듈 종속성의 암호화 체크섬을 저장합니다. 이를 통해 다른 사람이 프로젝트를 빌드할 때go.sum
이 생성되었을 때 사용된 것과 정확히 동일한 코드를 사용하도록 하여 악의적인 변경 또는 우발적인 종속성 변경을 방지합니다.
rsc.io/quote v1.5.2 h1
-
모듈 경로: 모든 Go 모듈에는 본질적으로 가져오기 경로인 "모듈 경로"가 있습니다. GitHub에서 호스팅되는 모듈의 경우 일반적으로
github.com/username/repo-name
입니다. 이 경로는go.mod
에서 사용되며go get
에서도 모듈을 찾는 데 사용됩니다. -
시맨틱 가져오기 버전 관리: Go Modules는 시맨틱 버전 관리 (
MAJOR.MINOR.PATCH
)를 채택합니다. 주요 버전 (v2, v3 등)의 경우 모듈 경로 자체에/vN
이 접미사로 추가됩니다 (예:github.com/go-redis/redis/v8
). 이를 통해 동일한 종속성의 서로 다른 주요 버전이 동일한 모듈의 종속성 그래프 내에서 공존할 수 있습니다. v2 모듈을 가져오는 신규 사용자는 자동으로 해당 패키지의v2
버전을 가져옵니다. -
GO111MODULE
환경 변수 (전환 지원):GOPATH
에서 Modules로 전환하는 동안GO111MODULE
은 Go 도구 체인의 동작을 제어했습니다.
auto
(기본값):$GOPATH/src
내에서GOPATH
모드를 사용합니다. 외부에서는go.mod
파일이 있는 경우 모듈 모드를 사용합니다.on
:$GOPATH/src
내에서도 항상 모듈 모드를 사용합니다.off
: 모듈 모드를 사용하지 않고 항상GOPATH
모드를 사용합니다. 오늘날 일반적으로 Go 1.16 이상이 사용되므로 모듈 모드는 거의 보편적으로 기본적으로on
이므로GO111MODULE
은 새 프로젝트와 관련이 없습니다.
Go Modules 사용
Go Modules를 사용한 워크플로는 직관적이고 강력합니다.
- 새 모듈 초기화:
프로젝트 디렉터리 (어디든
GOPATH/src
외부)로 이동하여 다음을 실행합니다.undefined
mkdir my-go-app
cd my-go-app
go mod init example.com/my-go-app # 모듈 경로
```
이렇게 하면 초기 go.mod
파일이 생성됩니다.
-
종속성 추가:
.go
파일에서 새 패키지를import
한 다음go build
,go run
또는go mod tidy
를 실행하면 Go 도구 체인이 누락된 종속성을 자동으로 감지하고 다운로드한 다음 최신 호환 버전으로go.mod
파일에 항목을 추가합니다.rsc.io/quote
를 사용하여main.go
를 만들어 보겠습니다.undefined
// my-go-app/main.go package main
import ( "fmt"
"rsc.io/quote" // 다운로드되어 go.mod에 추가됩니다.
)
func main() { fmt.Println(quote.Hello()) fmt.Println(quote.Go()) } ```
이제 실행합니다.
```bash
cd my-go-app
go run .
산출:
Hello, world.
Go is a general-purpose language designed with systems programming in mind.
```
go run .
(또는 go build
)를 실행한 후 go.mod
및 go.sum
을 검사합니다.
`my-go-app/go.mod`:
```
module example.com/my-go-app
go 1.22
require rsc.io/quote v1.5.2 ```
`my-go-app/go.sum` (간결성을 위해 자름):
```
rsc.io/quote v1.5.2 h1rsc.io/sampler
도 rsc.io/quote
의 전이적 종속성이므로 추가되었음을 알 수 있습니다.
- 명시적으로 종속성 추가/업데이트:
종속성을 특정 버전으로 명시적으로 추가하거나 업데이트할 수 있습니다.
undefined
go get github.com/gin-gonic/gin@v1.9.1 # 특정 버전 추가
go get github.com/gin-gonic/gin@latest # 최신 안정 버전으로 업데이트
go get github.com/gin-gonic/gin@master # 마스터 분기에서 가져오기
```
이러한 명령은 go.mod
및 go.sum
을 적절히 수정합니다.
- 사용하지 않는 종속성 정리:
undefined
go mod tidy
```
이 명령은 go.mod
및 go.sum
에서 사용하지 않는 종속성을 제거하여 종속성 그래프가 최소화되고 정확하도록 합니다.
-
벤더링 (선택 사항): 인터넷 액세스가 제한된 환경의 경우 종속성을 "벤더링"하여 프로젝트 내의
vendor/
디렉터리에 넣을 수 있습니다.undefined
go mod vendor
```
이후 빌드는 네트워크에서 가져오는 대신 벤더링된 종속성을 사용합니다 (단, GOFLAGS=-mod=vendor
가 설정되어 있거나 Go 버전의 경우 묵시적인 경우 < 1.14 (Go 1.14 이후에는 vendor
폴더가 있으면 벤더를 묵시적으로 사용)).
-
replace
지시어: 로컬 개발 또는 포크에 유용합니다. 로컬 또는 원격의 다른 경로로 모듈 종속성을 대체할 수 있습니다.
// go.mod module example.com/my-app
go 1.22
require ( example.com/my-dep v1.0.0 // 일반적으로 원격 리포지토리를 가리킵니다. )
replace example.com/my-dep v1.0.0 => ../my-dep-local // 로컬 버전 사용 // 또는 replace example.com/my-dep v1.0.0 => github.com/myuser/my-dep-fork v1.0.0 ```
Go Modules의 이점
- 재현 가능한 빌드:
go.mod
및go.sum
은 종속성 트리와 암호화 해시를 정확하게 정의하여 빌드가 매번, 모든 곳에서 동일하도록 합니다. - 버전 관리: 동일한 패키지의 다른 버전을 사용하기 위해 다른 프로젝트 (또는 동일한 프로젝트의 전이적 종속성의 다른 부분)를 허용하여 "종속성 지옥"을 해결합니다.
- 프로젝트 격리: 프로젝트는 자체 포함되어 있습니다. 파일 시스템의 아무 곳에서나 Go 모듈을 복제할 수 있으며
go build
는GOPATH
를 설정하거나 프로젝트를 그 안에 배치하지 않고도 작동합니다. - 단순화된
go get
:go get
은 이제 버전과 모듈을 이해하고 지정된 내용을 정확하게 가져옵니다. - 종속성 캐싱: 종속성은 전역 모듈 캐시 (일반적으로
$GOPATH/pkg/mod
)로 다운로드되므로 한 번만 다운로드되고 다른 프로젝트에서 재사용됩니다. - 프록시 지원 (
GOPROXY
):GOPROXY
를 사용하면 모듈의 캐시 및/또는 소스 역할을 하는 Go 모듈 프록시 서버를 구성하여 특히 기업 네트워크에서 안정성과 보안을 향상시킬 수 있습니다.go.sum
유효성 검사는 여전히 무결성을 보장합니다.
진화 이해: 패러다임 전환
GOPATH
에서 Go Modules로의 전환은 Go 프로젝트가 구성되고 관리되는 방식의 근본적인 변화를 나타냅니다.
- 전역에서 로컬로:
GOPATH
는 모든 프로젝트가 동일한 종속성 집합을 공유하는 전역 모놀리식 작업 공간을 부과했습니다. Go Modules는 이를 로컬의 프로젝트 중심 방식으로 전환하여 각 프로젝트의 종속성 및 해당 버전이 명시적으로 선언되고 격리됩니다. - 암시적에서 명시적으로:
GOPATH
는 디렉터리 구조를 기반으로 한 암시적 검색에 의존했습니다. Go Modules는go.mod
및go.sum
을 통해 종속성을 명시적으로 만들어 명확성과 제어 기능을 제공합니다. - "그냥 작동" (때로는)에서 재현 가능한 안정성으로:
GOPATH
는 충돌하는 종속성이 없는 그린필드 프로젝트에는 간단했지만 그 이상에서는 빠르게 골칫거리가 되었습니다. Go Modules는 안정성과 재현성을 우선시하며, 이는 강력한 소프트웨어 개발에 필수적입니다.
오늘날 새로운 Go 프로젝트는 거의 독점적으로 Go Modules를 사용해야 합니다. GOPATH
는 여전히 존재하며 Go 도구 체인 자체 또는 매우 오래된 프로젝트에 대한 목적을 수행하지만 애플리케이션 소스 코드 또는 종속성을 관리하는 데 권장되는 방법은 더 이상 아닙니다.
결론
GOPATH
에서 Go Modules로의 Go 종속성 관리의 진화는 개발자의 고충을 해결하고 생태계를 성숙시키기 위한 언어의 헌신에 대한 증거입니다. GOPATH
는 Go의 형성기에 그 목적을 달성하여 간단한 규칙을 확립했습니다. 그러나 Go가 더 크고 복잡한 시스템에서 견인력을 얻으면서 그 한계가 분명해졌습니다.
Go Modules는 버전 관리, 격리 및 재현성의 문제를 우아하게 해결하여 다른 언어 생태계의 최신 패키지 관리자와 동등한 강력한 기본 제공 솔루션을 제공합니다. 이러한 변환은 안정적이고 유지 관리 가능하며 확장 가능한 애플리케이션을 구축하기 위한 Go의 매력을 크게 향상시켜 Go 개발자 경험을 그 어느 때보다 부드럽고 효율적으로 만들었습니다. 이 진화를 이해하는 것은 모든 Go 개발자에게 매우 중요하며 Go의 최신 도구의 모든 기능을 활용할 수 있습니다.