Go 워크스페이스 구조: GOPATH에서 go.work까지

현대적인 워크스페이스로 Go 프로젝트를 효율적으로 정리하세요.

Page content

Go 프로젝트 관리은 작업 공간이 코드, 의존성 및 빌드 환경을 어떻게 조직하는지를 이해하는 것이 효과적인 방법입니다.

Go의 접근 방식은 크게 변화했습니다. 엄격한 GOPATH 시스템에서 유연한 모듈 기반 워크플로우로, 마침내 Go 1.18의 워크스페이스 기능이 다중 모듈 개발을 우아하게 처리하게 되었습니다.

gopher’s workplace

Go 워크스페이스 진화 이해

Go의 워크스페이스 모델은 세 가지 구분된 시대를 거쳤으며, 각 시대는 이전 버전의 한계를 해결하면서도 뒷받침되는 호환성을 유지했습니다.

GOPATH 시대 (Go 1.11 이전)

처음에는 Go가 GOPATH 환경 변수를 중심으로 한 엄격한 워크스페이스 구조를 강제했습니다:

$GOPATH/
├── src/
│   ├── github.com/
│   │   └── username/
│   │       └── project1/
│   ├── gitlab.com/
│   │   └── company/
│   │       └── project2/
│   └── ...
├── bin/      # 컴파일된 실행 파일
└── pkg/      # 컴파일된 패키지 객체

모든 Go 코드는 $GOPATH/src에 있어야 하며, import 경로에 따라 정리되어야 했습니다. 이는 예측 가능성을 제공했지만, 다음과 같은 많은 마찰을 일으켰습니다:

  • 버전 관리 없음: 한 번에 하나의 의존성 버전만 사용할 수 있었습니다.
  • 글로벌 워크스페이스: 모든 프로젝트가 의존성을 공유함으로써 충돌이 발생했습니다.
  • 경직된 구조: GOPATH 외부에 프로젝트가 존재할 수 없었습니다.
  • Vendor 지옥: 다른 버전을 관리하기 위해 복잡한 vendor 디렉터리가 필요했습니다.

Go 모듈 시대 (Go 1.11+)

Go 모듈은 go.modgo.sum 파일을 도입하여 프로젝트 관리를 혁신적으로 바꾸었습니다:

myproject/
├── go.mod          # 모듈 정의 및 의존성
├── go.sum          # 암호화된 해시
├── main.go
└── internal/
    └── service/

주요 장점:

  • 프로젝트는 파일 시스템의 어디에나 존재할 수 있습니다.
  • 각 프로젝트는 명시적인 버전으로 의존성을 관리합니다.
  • 해시를 통해 재현 가능한 빌드가 가능합니다.
  • 세마포트 버전 관리 지원 (v1.2.3)
  • 로컬 개발을 위한 대체 지시문

모듈을 초기화하려면:

go mod init github.com/username/myproject

Go 명령어 및 모듈 관리에 대한 종합적인 참조를 원하시면 Go Cheatsheet를 확인해 보세요.

GOPATH와 Go 워크스페이스의 차이점은 무엇인가요?

기본적인 차이점은 범위와 유연성에 있습니다. GOPATH는 특정 디렉터리 구조에 모든 코드가 있어야 하는 하나의 글로벌 워크스페이스였습니다. 버전 관리 개념이 없었기 때문에, 동일한 패키지가 다른 프로젝트에서 다른 버전을 필요로 할 때 의존성 충돌이 발생했습니다.

Go 1.18에서 도입된 go.work 파일을 사용하는 현대의 Go 워크스페이스는 여러 모듈을 함께 관리하는 로컬, 프로젝트 중심의 워크스페이스를 제공합니다. 각 모듈은 명시적인 버전 관리를 위한 자체 go.mod 파일을 유지하고, go.work는 로컬 개발을 위해 이를 조정합니다. 이를 통해 다음과 같은 작업이 가능해집니다:

  • 라이브러리와 소비자 동시에 작업
  • 중간 버전을 출판하지 않고 상호 의존 모듈을 개발
  • 커밋 전 여러 모듈 간 변경 사항 테스트
  • 각 모듈을 독립적으로 버전 관리 및 배포

가장 중요한 점은 워크스페이스가 선택적 개발 도구라는 것입니다. 워크스페이스 없이도 모듈은 완벽하게 작동하며, GOPATH는 필수적이었던 것과 달리 선택적입니다.

현대 워크스페이스: go.work 파일

Go 1.18은 여러 관련 모듈을 로컬에서 개발할 때 발생하는 일반적인 문제를 해결하기 위해 워크스페이스를 도입했습니다.

언제 go.mod 대신 go.work 파일을 사용해야 하나요?

다른 모듈과 상호작용하는 모듈을 동시에 개발하고 있을 때 go.work를 사용하세요. 일반적인 시나리오는 다음과 같습니다:

모노리포 개발: 단일 저장소 내 여러 서비스가 서로 참조하는 경우.

라이브러리 개발: 라이브러리를 개발하고 소비자 애플리케이션에서 테스트하고 싶은 경우.

마이크로 서비스: 공통 내부 패키지가 있는 여러 서비스를 수정하는 경우.

오픈소스 기여: 의존성을 수정하고 동시에 애플리케이션에서 테스트하고 싶은 경우.

go.work를 사용하지 않는 경우:

  • 단일 모듈 프로젝트 (단지 go.mod 사용)
  • 프로덕션 빌드 (워크스페이스는 개발용만)
  • 모든 의존성이 외부에 있으며 안정적인 경우

워크스페이스 생성 및 관리

워크스페이스 초기화:

cd ~/projects/myworkspace
go work init

이 명령은 빈 go.work 파일을 생성합니다. 이제 모듈을 추가하세요:

go work use ./api
go work use ./shared
go work use ./worker

또는 현재 디렉터리에 있는 모든 모듈을 재귀적으로 추가하세요:

go work use -r .

결과 go.work 파일:

go 1.21

use (
    ./api
    ./shared
    ./worker
)

워크스페이스 작동 방식

go.work 파일이 존재하는 경우 Go 도구체인은 의존성을 해결하기 위해 이를 사용합니다. 모듈 apishared를 가져오는 경우, Go는 외부 저장소를 확인하기 전에 워크스페이스를 먼저 확인합니다.

예시 워크스페이스 구조:

myworkspace/
├── go.work
├── api/
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── shared/
│   ├── go.mod
│   └── auth/
│       └── auth.go
└── worker/
    ├── go.mod
    └── main.go

api/main.go에서 shared/auth를 직접 가져올 수 있습니다:

package main

import (
    "fmt"
    "myworkspace/shared/auth"
)

func main() {
    token := auth.GenerateToken()
    fmt.Println(token)
}

shared/auth에 대한 변경 사항은 출판 없이도 api에 즉시 표시됩니다.

go.work 파일을 버전 관리에 포함해야 하나요?

아니요—절대 포함하지 마세요. go.work 파일은 로컬 개발 도구이며, 프로젝트 아티팩트가 아닙니다. 그 이유는 다음과 같습니다:

경로 구체성: go.work는 존재하지 않는 다른 머신이나 CI/CD 시스템에 대한 로컬 파일 경로를 참조합니다.

빌드 재현성: 프로덕션 빌드는 go.mod만 사용하여 일관된 의존성 해결을 보장해야 합니다.

개발자 유연성: 각 개발자는 로컬 워크스페이스를 다르게 구성할 수 있습니다.

CI/CD 호환성: 자동화된 빌드 시스템은 go.mod 파일만 기대합니다.

항상 go.workgo.work.sum.gitignore에 추가하세요:

# .gitignore
go.work
go.work.sum

CI/CD 파이프라인 및 다른 개발자는 각 모듈의 go.mod 파일을 사용하여 빌드하므로, 환경 간 재현 가능한 빌드가 보장됩니다.

실용적인 워크스페이스 패턴

패턴 1: 여러 서비스가 있는 모노리포

company-platform/
├── go.work
├── cmd/
│   ├── api/
│   │   ├── go.mod
│   │   └── main.go
│   ├── worker/
│   │   ├── go.mod
│   │   └── main.go
│   └── scheduler/
│       ├── go.mod
│       └── main.go
├── internal/
│   ├── auth/
│   │   ├── go.mod
│   │   └── auth.go
│   └── database/
│       ├── go.mod
│       └── db.go
└── pkg/
    └── logger/
        ├── go.mod
        └── logger.go

다중 테넌트 애플리케이션에서 데이터베이스 고립이 필요한 경우, Multi-Tenancy Database Patterns with examples in Go를 참조하세요.

각 구성 요소는 자체 go.mod를 가진 독립 모듈입니다. 워크스페이스는 이를 조정합니다:

go 1.21

use (
    ./cmd/api
    ./cmd/worker
    ./cmd/scheduler
    ./internal/auth
    ./internal/database
    ./pkg/logger
)

모노리포 설정에서 API 서비스를 빌드할 때, 엔드포인트를 적절히 문서화하는 것이 필수적입니다. Adding Swagger to Your Go API에 대해 더 알아보세요.

패턴 2: 라이브러리 및 소비자 개발

mylib를 개발하고 myapp에서 테스트하고 싶은 경우:

dev/
├── go.work
├── mylib/
│   ├── go.mod       # module github.com/me/mylib
│   └── lib.go
└── myapp/
    ├── go.mod       # module github.com/me/myapp
    └── main.go      # github.com/me/mylib를 가져옵니다.

워크스페이스 파일:

go 1.21

use (
    ./mylib
    ./myapp
)

mylib에 대한 변경 사항은 GitHub에 출판하지 않고도 myapp에서 즉시 테스트할 수 있습니다.

패턴 3: 포크 개발 및 테스트

의존성을 포크하여 버그를 수정하고 싶은 경우:

projects/
├── go.work
├── myproject/
│   ├── go.mod       # github.com/upstream/lib를 사용
│   └── main.go
└── lib-fork/
    ├── go.mod       # module github.com/upstream/lib
    └── lib.go       # 당신의 버그 수정

워크스페이스는 포크 테스트를 가능하게 합니다:

go 1.21

use (
    ./myproject
    ./lib-fork
)

github.com/upstream/lib는 로컬 ./lib-fork 디렉터리로 해석됩니다.

개발 머신에서 여러 Go 프로젝트를 어떻게 조직해야 하나요?

최적의 조직 전략은 개발 스타일과 프로젝트 관계에 따라 달라집니다.

전략 1: 평평한 프로젝트 구조

관련 없는 프로젝트는 별도로 유지하세요:

~/dev/
├── personal-blog/
│   ├── go.mod
│   └── main.go
├── work-api/
│   ├── go.mod
│   └── cmd/
├── side-project/
│   ├── go.mod
│   └── server.go
└── experiments/
    └── ml-tool/
        ├── go.mod
        └── main.go

각 프로젝트는 독립적입니다. 워크스페이스는 필요하지 않으며, 각 프로젝트는 go.mod를 통해 의존성을 관리합니다.

전략 2: 도메인 그룹화

관련된 프로젝트를 도메인 또는 목적에 따라 그룹화하세요:

~/dev/
├── work/
│   ├── platform/
│   │   ├── go.work
│   │   ├── api/
│   │   ├── worker/
│   │   └── shared/
│   └── tools/
│       ├── deployment-cli/
│       └── monitoring-agent/
├── open-source/
│   ├── go-library/
│   └── cli-tool/
└── learning/
    ├── algorithms/
    └── design-patterns/

도메인 내에서 관련된 프로젝트에 워크스페이스 (go.work)를 사용하지만, 관련 없는 프로젝트는 별도로 유지하세요. 워크스페이스에서 CLI 도구를 구축하는 경우, Building CLI Applications in Go with Cobra & Viper를 참조하세요.

전략 3: 클라이언트 또는 조직 기반

프리랜서 또는 컨설턴트가 여러 클라이언트를 관리하는 경우:

~/projects/
├── client-a/
│   ├── ecommerce-platform/
│   └── admin-dashboard/
├── client-b/
│   ├── go.work
│   ├── backend/
│   ├── shared-types/
│   └── worker/
└── internal/
    ├── my-saas/
    └── tools/

프로젝트가 상호 의존적인 경우, 클라이언트별로 워크스페이스를 생성하세요.

조직 원칙

중첩 깊이 제한: 2~3개의 디렉터리 수준으로 유지하세요. 깊은 계층 구조는 복잡하게 됩니다.

의미 있는 이름 사용: ~/dev/platform/~/p1/보다 명확합니다.

관심 분리: 작업, 개인, 실험, 오픈소스 기여를 구분하세요.

구조 문서화: 개발 폴더 루트에 README.md를 추가하여 조직을 설명하세요.

일관된 규칙: 모든 프로젝트에서 동일한 구조 패턴을 사용하여 근육 기억을 형성하세요.

Go 워크스페이스 사용 시 일반적인 실수는 무엇인가요?

실수 1: go.work를 Git에 커밋

이전에 논의했듯이, 이는 다른 개발자와 CI/CD 시스템에 대한 빌드를 깨뜨립니다. 항상 .gitignore에 추가하세요.

실수 2: 모든 명령이 go.work를 존중한다고 기대

모든 Go 명령이 go.work를 존중하지 않습니다. 특히 go mod tidy는 개별 모듈에 작동하며, 워크스페이스에 작동하지 않습니다. 모듈 내에서 go mod tidy를 실행하면 워크스페이스 내에 존재하는 의존성을 가져오려 시도하여 혼란을 일으킬 수 있습니다.

해결책: 각 모듈 디렉터리에서 go mod tidy를 실행하거나 다음을 사용하세요:

go work sync

이 명령은 go.work를 업데이트하여 모듈 간 일관성을 보장합니다.

실수 3: 잘못된 Replace 지시문

go.modgo.workreplace 지시문을 사용하면 충돌이 발생할 수 있습니다:

# go.work
use (
    ./api
    ./shared
)

replace github.com/external/lib => ../external-lib  # 워크스페이스에 대한 올바른 방식

# api/go.mod
replace github.com/external/lib => ../../../somewhere-else  # 충돌!

해결책: 워크스페이스를 사용할 때 go.mod 파일 대신 go.workreplace 지시문을 배치하세요.

실수 4: 워크스페이스 없이 테스트하지 않음

로컬에서 go.work를 사용할 때 코드가 작동하지만, 워크스페이스가 없는 프로덕션 또는 CI에서 실패할 수 있습니다.

해결책: 주기적으로 워크스페이스를 비활성화한 상태에서 빌드 테스트를 수행하세요:

GOWORK=off go build ./...

이 명령은 프로덕션에서 코드가 어떻게 빌드되는지 시뮬레이션합니다.

실수 5: GOPATH 및 모듈 모드 혼합

일부 개발자는 오래된 GOPATH 프로젝트를 유지하면서 다른 곳에서 모듈을 사용하여 혼란을 일으킬 수 있습니다.

해결책: 모듈로 완전히 이전하세요. 반드시 오래된 GOPATH 프로젝트를 유지해야 한다면 gvm 또는 Docker 컨테이너를 사용하여 환경을 분리하세요.

실수 6: go.work.sum을 잊음

go.sum과 마찬가지로 워크스페이스는 go.work.sum을 생성하여 의존성을 검증합니다. 커밋하지 않아도 되지만, 삭제하지 마세요. 이는 개발 중 재현 가능한 빌드를 보장합니다.

실수 7: 너무 넓은 워크스페이스

관련 없는 모듈을 워크스페이스에 추가하면 빌드 속도가 느려지고 복잡성이 증가합니다.

해결책: 워크스페이스는 밀접하게 관련된 모듈에 집중하세요. 모듈이 상호작용하지 않는다면, 워크스페이스를 공유할 필요가 없습니다.

고급 워크스페이스 기술

Replace 지시문 사용

go.workreplace 지시문은 모듈 임포트를 재지정합니다:

go 1.21

use (
    ./api
    ./shared
)

replace (
    github.com/external/lib v1.2.3 => github.com/me/lib-fork v1.2.4
    github.com/another/lib => ../local-another-lib
)

이 기능은 다음과 같은 작업에 강력합니다:

  • 포크된 의존성을 테스트
  • 외부 라이브러리의 로컬 버전 사용
  • 임시로 대체 구현 사용

다중 버전 테스트

의존성의 여러 버전을 사용하여 라이브러리를 테스트하세요:

# 터미널 1: v1.x 버전으로 테스트
GOWORK=off go test ./...

# 터미널 2: 로컬 수정된 의존성으로 테스트
go test ./...  # go.work 사용

Vendor 디렉터리와 함께 워크스페이스

워크스페이스와 vendoring은 함께 존재할 수 있습니다:

go work vendor

이 명령은 전체 워크스페이스에 대한 vendor 디렉터리를 생성하며, 공기 분리 환경이나 재현 가능한 오프라인 빌드에 유용합니다.

IDE 통합

대부분의 IDE는 Go 워크스페이스를 지원합니다:

VS Code: Go 확장을 설치하세요. go.work 파일을 자동으로 감지합니다.

GoLand: 워크스페이스 루트 디렉터리를 열고, GoLand는 go.work를 인식하고 프로젝트를 구성합니다.

Vim/Neovim with gopls: gopls 언어 서버는 go.work를 자동으로 존중합니다.

IDE가 “모듈을 찾을 수 없음” 오류를 표시하지만 워크스페이스가 올바르다면 다음을 시도하세요:

  • 언어 서버를 재시작
  • go.work 경로가 올바른지 확인
  • gopls가 최신인지 확인

GOPATH에서 모듈로의 이전

GOPATH를 여전히 사용 중이라면, 다음과 같이 부드럽게 이전하세요:

단계 1: Go 업데이트

Go 1.18 이상을 사용하고 있는지 확인하세요:

go version

단계 2: GOPATH 외부로 프로젝트 이동

프로젝트는 더 이상 $GOPATH/src에 있어야 하지 않습니다. 어디든 이동하세요:

mv $GOPATH/src/github.com/me/myproject ~/dev/myproject

단계 3: 모듈 초기화

각 프로젝트에서:

cd ~/dev/myproject
go mod init github.com/me/myproject

프로젝트가 dep, glide, 또는 vendor를 사용하는 경우, go mod init은 자동으로 의존성을 go.mod로 전환합니다.

단계 4: 의존성 정리

go mod tidy      # 사용하지 않는 의존성 제거
go mod verify    # 해시 확인

단계 5: 임포트 경로 업데이트

모듈 경로가 변경된 경우, 전체 코드베이스의 임포트를 업데이트하세요. gofmtgoimports 도구가 도움이 됩니다:

gofmt -w .
goimports -w .

단계 6: 철저한 테스트

go test ./...
go build ./...

모든 것이 컴파일되고 테스트가 통과하는지 확인하세요. 테스트를 효과적으로 구성하는 방법에 대한 종합적인 지침은 Go Unit Testing: Structure & Best Practices를 참조하세요.

단계 7: CI/CD 업데이트

CI/CD 스크립트에서 GOPATH 특정 환경 변수를 제거하세요. 현대 Go 빌드는 그들을 필요로 하지 않습니다:

# 이전 (GOPATH)
env:
  GOPATH: /go
  PATH: /go/bin:$PATH

# 새 (모듈)
env:
  GO111MODULE: on  # 선택적, Go 1.13+에서 기본값

단계 8: GOPATH 제거 (선택적)

완전히 이전한 후 GOPATH 디렉터리를 제거할 수 있습니다:

rm -rf $GOPATH
unset GOPATH  # .bashrc 또는 .zshrc에 추가

최선 실천 요약

  1. 모든 새 프로젝트에 모듈 사용: Go 1.13 이후로 표준이며, 더 나은 의존성 관리를 제공합니다.

  2. 필요할 때만 워크스페이스 생성: 다중 모듈 개발 시 go.work 사용. 단일 프로젝트는 필요하지 않습니다.

  3. go.work 파일을 절대 커밋하지 마세요: 개인 개발 도구이며, 프로젝트 아티팩트가 아닙니다.

  4. 논리적으로 프로젝트를 조직하세요: 도메인, 클라이언트, 목적에 따라 그룹화하고 계층을 얕게 유지하세요.

  5. 워크스페이스 구조를 문서화하세요: 조직을 설명하는 README 파일을 추가하세요.

  6. 워크스페이스 없이 주기적으로 테스트하세요: go.work가 활성화되지 않은 상태에서 코드가 올바르게 빌드되는지 확인하세요.

  7. 워크스페이스는 집중적으로 유지하세요: 관련된, 상호 의존 모듈만 포함하세요.

  8. replace 지시문을 신중하게 사용하세요: 로컬 대체를 위해 go.work에 배치하고 go.mod에 배치하지 마세요.

  9. go work sync 실행: 워크스페이스 메타데이터를 모듈 의존성과 일관되게 유지하세요.

  10. Go 버전을 최신으로 유지하세요: 각 릴리스마다 워크스페이스 기능이 개선됩니다.

유용한 링크