고 기초 #7 패키지와 모듈 (go mod)
기초 시리즈 마지막 편. 지금까지의 코드는 한 파일에서 살아 있었지만, 실제 프로젝트는 여러 파일과 외부 라이브러리가 함께 동작합니다. 이번 글은 — Go의 코드 조직 시스템을 정리합니다.
두 가지 단위 — 패키지와 모듈 #
| 패키지 (package) | 모듈 (module) | |
|---|---|---|
| 단위 | 한 폴더 | 여러 폴더의 묶음 |
| 정의 | package 이름 선언 | go.mod 파일 |
| 비유 | 클래스/네임스페이스 | 프로젝트/라이브러리 |
한 폴더 = 한 패키지, 여러 폴더 + go.mod = 한 모듈. 모듈이 더 큰 단위입니다.
패키지 — 한 폴더가 한 단위 #
같은 폴더의 파일들은 모두 같은 패키지에 속해야 합니다.
calculator/
├── add.go (package calculator)
├── multiply.go (package calculator)
└── helper.go (package calculator)모두 package calculator로 시작해야 합니다. 다른 패키지 이름이 섞이면 컴파일 에러.
main 패키지 — 실행 가능한 프로그램 #
package main
import "fmt"
func main() {
fmt.Println("hi")
}package main + func main()가 있는 폴더는 실행 가능한 프로그램. 그 외 패키지는 라이브러리로 다른 코드가 import 해서 쓰는 코드입니다.
가시성 — 대문자 첫 글자 #
Go의 가시성은 매우 단순합니다.
이름이 대문자로 시작하면 외부에 노출(exported), 소문자면 패키지 안에서만 보임(unexported).
package math
// 외부에서 사용 가능
func Add(a, b int) int {
return a + b
}
// 같은 패키지에서만 사용 가능
func square(n int) int {
return n * n
}
// 외부 노출 상수
const Pi = 3.14159
// 내부 상수
const initialBuffer = 1024다른 언어처럼 public/private 키워드가 따로 없습니다. 이름 첫 글자만 봐도 가시성을 알 수 있습니다.
import — 다른 패키지 가져오기 #
표준 라이브러리 #
import "fmt"
import "strings"
import "time"
// 또는 그룹으로
import (
"fmt"
"strings"
"time"
)여러 import는 그룹 형태가 권장됩니다. 알파벳순 정렬은 goimports가 자동으로 해 줍니다.
외부 패키지 #
import (
"github.com/google/uuid"
"github.com/spf13/cobra"
)GitHub URL 같은 모양이 그대로 import path가 됩니다. Go의 모듈 시스템이 URL 기반인 이유입니다.
별칭과 도트 import #
import (
f "fmt" // 별칭
. "strings" // 도트 — 패키지 이름 없이 직접 사용 (권장 X)
_ "github.com/.../init" // 빈 식별자 — 부수효과만 (init 함수 실행)
)
f.Println("hi")별칭은 같은 이름 충돌 시 유용. 도트 import는 코드가 어디서 온 함수인지 알기 어려워져 거의 안 씁니다. 빈 식별자(_)는 패키지의 init 함수만 실행하고 싶을 때(드라이버 등록 등).
모듈 — go.mod
#
여러 패키지를 묶은 단위. go.mod 파일이 한 모듈의 진입점입니다.
mkdir myapp
cd myapp
go mod init github.com/curtis/myappgo.mod가 만들어집니다.
module github.com/curtis/myapp
go 1.22- module — 이 모듈의 import path. 다른 코드가
import "github.com/curtis/myapp/..."식으로 가져옴. - go — 사용하는 Go 최소 버전.
모듈 이름 — 왜 GitHub 경로인가? #
Go는 모듈을 URL로 식별합니다. 보통 GitHub/GitLab의 저장소 경로가 그대로 모듈 이름이 됩니다.
module github.com/curtis/myapp
module gitlab.com/team/service
module example.com/internal/utils // 사적 도메인GitHub에 올릴 계획이 없어도 그 형식을 따릅니다. Go가 의존성을 다운로드할 때 그 URL로 가서 가져옵니다(공개 저장소일 경우).
여러 패키지가 있는 모듈 #
myapp/
├── go.mod (module github.com/curtis/myapp)
├── main.go (package main)
├── auth/
│ ├── login.go (package auth)
│ └── register.go (package auth)
└── db/
└── connection.go (package db)main.go에서 다른 패키지 사용:
package main
import (
"fmt"
"github.com/curtis/myapp/auth"
"github.com/curtis/myapp/db"
)
func main() {
conn := db.Connect()
user := auth.Login("커티스", "secret")
fmt.Println(conn, user)
}같은 모듈 안의 패키지는 — 모듈 이름 + 폴더 경로로 import.
외부 패키지 추가 — go get
#
go get github.com/google/uuid이 명령이:
- 패키지를 다운로드
go.mod에 의존성 추가go.sum에 체크섬 기록
module github.com/curtis/myapp
go 1.22
require github.com/google/uuid v1.6.0go.sum은 의존성의 정확한 버전을 기록한 파일 — 다른 사람이 같은 코드를 빌드해도 정확히 같은 버전이 쓰이도록 보장합니다.
사용 #
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
id := uuid.New()
fmt.Println(id)
}go mod tidy — 정리
#
go mod tidy코드를 분석해서:
- 사용 중인데 go.mod에 없는 의존성 → 추가
- go.mod에 있는데 안 쓰는 의존성 → 제거
작업 끝마다 한 번씩 실행하는 습관이 좋습니다.
의존성 버전 — semver #
require github.com/google/uuid v1.6.0semver(시맨틱 버저닝) — v메이저.마이너.패치. Go 모듈 시스템은 다음 규칙을 따릅니다.
- 메이저 0 — 안정 보장 없음 (실험)
- 메이저 1+ — 호환 보장 (마이너/패치 업데이트는 호환)
- 메이저 2+ — import path에
/v2,/v3같은 suffix 필수
import "github.com/some/lib/v2"라이브러리가 v2로 올라가면 import path 자체가 바뀝니다. 하위 호환을 강제하는 디자인입니다.
버전 업데이트 #
# 패키지 최신으로
go get github.com/google/uuid@latest
# 특정 버전
go get github.com/google/uuid@v1.5.0
# 모든 의존성 최신
go get -u ./...
# 마이너/패치만 최신
go get -u=patch ./...같은 모듈 안에서 — internal 패키지 #
myapp/
├── go.mod
├── main.go
├── api/
│ └── handler.go (package api)
└── internal/
└── auth/
└── login.go (package auth)internal/ 폴더 안의 패키지는 — 그 모듈 또는 그 모듈의 자식만 import가능. 외부 모듈은 못 가져옵니다.
라이브러리를 만들 때 — 노출 API와 구현 디테일을 분리하는 표준 패턴입니다.
패키지 init 함수 #
각 패키지는 — 처음 import 될 때 자동 실행되는 init() 함수를 가질 수 있습니다.
package config
import "log"
var Settings map[string]string
func init() {
Settings = loadFromFile()
log.Println("config로드됨")
}부수효과(설정 로드, 드라이버 등록)에 쓰입니다. 한 패키지에 여러 init도 가능합니다.
남용은 피하세요 — 명시적인 초기화 함수(Init())가 보통 더 명확합니다.
vendor 폴더 — 옵션 #
go mod vendor 명령으로 의존성을 프로젝트 안에 복사할 수 있습니다.
go mod vendorvendor/ 폴더에 모든 의존성 코드가 들어옵니다. 빌드 시 그곳에서 가져옵니다.
장점: 외부 다운로드 없이 빌드 가능 (오프라인, 사설 네트워크에서). 단점: 저장소가 커짐.
요즘은 go.mod + 로컬 캐시($GOPATH/pkg/mod)로 충분해서 vendor는 잘 안 쓰는 추세입니다.
replace — 로컬 경로로 대체
#
라이브러리 개발 중 — 실제 의존성을 로컬 경로로 임시 대체.
require github.com/curtis/lib v1.0.0
replace github.com/curtis/lib => ../lib../lib 폴더의 코드가 사용됩니다. 라이브러리와 그것을 쓰는 앱을 동시에 개발할 때 유용합니다. 커밋 전에 보통 제거합니다.
자주 쓰는 명령 정리 #
go mod init <이름> # 새 모듈
go mod tidy # 의존성 정리
go mod download # 명시된 의존성 다운로드만
go get <패키지> # 의존성 추가
go get -u <패키지> # 업데이트
go mod why <패키지> # 왜 이 의존성이 필요한가
go mod graph # 의존성 그래프 출력
go mod vendor # vendor 폴더 만들기가벼운 가이드 — 패키지 디자인 #
큰 디자인 가이드를 모두 정리하긴 어렵지만, 자주 도움 되는 것들을 추렸습니다.
- 패키지 이름은 짧고 소문자 —
auth,db,config - 패키지가 여러 일을 하면 잘게 쪼개기
- 순환 의존(A → B, B → A)은 컴파일 에러 — 디자인이 꼬였다는 신호
- 외부 노출은 최소화 — 가능한 소문자 시작
util,common같은 잡동사니 패키지는 피하기 — 의도가 안 보임
마무리 #
기초 시리즈 7편을 거치며 다룬 내용:
- 시작과 첫 프로그램 — 셋업과 Hello World (#1)
- 변수, 타입, 상수 —
:=, iota, untyped const (#2) - 제어 흐름 — for가 유일, switch fallthrough 없음 (#3)
- 함수, 다중 반환, error —
if err != nil, defer (#4) - 컬렉션 — slice의 동작 원리, map의 comma-ok (#5)
- 구조체와 메서드 — 값/포인터 리시버, 임베딩 (#6)
- 패키지와 모듈 — 가시성, go.mod, internal (이번 글)
여기까지 잡으면 작은 프로그램과 CLI 도구는 자신 있게 만들 수 있습니다. 다음 중급 시리즈에서는 — 인터페이스, 동시성(고루틴/채널), 컨텍스트, 표준 라이브러리 같은 Go의 진짜 강점들을 다룹니다.