Go 모듈 작동 방식

김유경·2025년 11월 23일

들어가며

go get 명령어을 사용했는데 최신 코드가 내려오지 않는 문제가 있었습니다. 처음에는 당연히 github.com/~ 형태의 모듈이라 GitHub에서 직접 가져오는 줄 알았지만, 실제로는 다른 방식으로 동작하고 있었습니다.

👀 이 글에서는 Go 모듈 시스템이 어떻게 동작하는지 정리해보겠습니다.


Go 모듈이란?

Go 모듈은 관련 Go 패키지들의 집합으로, 프로젝트 루트에 위치한 go.mod 파일로 정의됩니다. 이 파일에는 모듈 경로, 필요한 Go 버전, 그리고 프로젝트가 의존하는 모듈과 그 버전 정보가 명시되어 있어 종속성 관리를 쉽게 할 수 있습니다.

module ebpf-route

go 1.25.0

require (
	github.com/cilium/ebpf v0.15.0
	github.com/spf13/cobra v1.10.1
	github.com/spf13/viper v1.21.0
)

require (
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect

go.mod 파일 구조

1) require: 프로젝트가 직접적으로 의존하는 모듈과 버전을 선언합니다.

2) replace: 특정 모듈을 다른 경로나 버전으로 대체할 때 사용합니다. 

3) exclude: 특정 버전의 모듈을 의존성 해석에서 제외할 때 사용합니다.

go.sum 파일 역할

go.sum 파일은 각 모듈 버전의 암호화된 해시(체크섬)를 저장합니다. 이 정보를 통해 동일한 버전의 모듈이 항상 동일한 내용을 갖는지 검증하며, 악의적인 코드 변경을 방지할 수 있습니다.

go mod tidy, go mod download, go build 명령어를 실행할 때 자동으로 업데이트됩니다.


go.get 동작 방식

go get rsc.io/quote@v1.5.2을 실행하면 GitHub에서 직접 가져올 것 같았지만, 그렇지 않았습니다 🫢

1) https://rsc.io/quote?go-get=1에 접속하여 태그를 조회합니다.

2) proxy.golang.org에서 캐시된 모듈을 먼저 조회합니다.

3) sum.golang.org에서 체크섬을 검증합니다.

4) go.sum에 해시를 기록하고 로컬에 모듈을 저장합니다.

Go 모듈 공용 인프라

1) proxy.golang.org (모듈 프록시)

모듈 다운로드 속도를 높이기 위한 캐시 서버로, go get 실행 시 기본적으로 이 프록시를 먼저 조회합니다. 원본 저장소가 접근 불가능하더라도 모듈을 다운로드할 수 있습니다.

2) sum.golang.org (체크섬 데이터베이스)

공개 Go 모듈의 모든 버전에 대한 해시 정보를 저장하는 투명 로그 기반 데이터베이스입니다.

3) index.golang.org (인덱스 서비스)

프록시가 캐싱한 모듈 버전들의 타임라인 피드를 제공합니다.

❓ 그렇다면 여기서 왜 체크썸 데이터베이스가 필요할까?라는 궁금증이 생겼습니다

기존의 go get은 다운로드한 코드가 원래 의도한 코드인지 검증하지 못했습니다. 즉, 공격자가 저장소나 네트워크 중간에서 코드를 바꿔도 이를 탐지할 방법이 없었습니다.

물론 go.sum이 도입되면서 로컬 차원의 검증은 가능해졌지만,
최초 다운로드 시점에는 비교할 대상이 없어 무결성을 보장할 수 없다는 한계가 있었습니다.

이 문제를 해결하기 위해 sum.golang.org(체크섬 데이터베이스) 가 도입되었습니다. 이 데이터베이스는 공개된 모든 Go 모듈 버전의 해시를 투명 로그 형태로 기록하며, 한 번 기록된 해시는 수정하거나 삭제할 수 없습니다.

따라서 공격자가 GitHub 저장소를 해킹하거나 네트워크를 변조해 코드를 바꾸더라도, 기록된 해시와 달라지는 순간 즉시 위조 사실이 드러납니다. 이 구조 덕분에 Go는 안전하고 신뢰할 수 있는 종속성 관리 시스템을 구현할 수 있게 되었습니다.


문제 분석

외부 모듈을 사용하던 프로젝트에서, 해당 모듈에 작은 수정사항을 적용한 뒤 기존과 동일한 버전(tag)으로 다시 배포하고 싶은 요구사항이 있었습니다. 즉, 새로운 버전을 만들지 않고 같은 태그 아래 변경된 코드를 덮어쓰는 방식을 기대했습니다.

처음에는 GitHub에서 코드를 가져올 것이므로 변경된 내용이 바로 반영될 것이라고 예상했지만, 실제로는 어떤 방법을 써도 이전 코드만 내려왔습니다. 로컬 캐시 삭제, 프록시 비활성화, 체크섬 검증 비활성화 등 다양한 시도를 했음에도 결과는 동일했습니다. 🥲

그 이유는 Go에서는 한 번 공개된 태그는 그 시점의 코드로 영원히 고정되기 때문입니다. 모듈이 처음 배포되는 순간 코드 내용 → 체크섬 → 버전 정보가 프록시 서버와 체크섬 데이터베이스에 기록되며, 이 값은 변경할 수 없습니다.

물론, go.sum을 삭제하고 체크섬 검증을 완전히 끄면 검증 절차가 사라지기 때문에 변경된 코드를 강제로 내려받을 수는 있었습니다. 하지만 이 방법은 검증 체계를 우회하는 상황일 뿐, Go 모듈의 보안 모델에 맞지 않습니다.

# 1. 모듈 캐시 삭제
go clean -modcache

# 2. go.sum 삭제
rm go.sum

# 3. 프록시/검증 끄고 재다운로드
GOPROXY=direct GOSUMDB=off go get github.com/~
go mod tidy

❗️ 결국 같은 버전을 다시 배포하는 방식은 Go에서 지원되지 않으며, 외부 모듈을 수정해야 한다면 항상 새로운 태그를 생성하는 것이 해결책입니다.

0개의 댓글