고 기초 #7 패키지와 모듈 (go mod)

6 분 소요

기초 시리즈 마지막 편. 지금까지의 코드는 한 파일에서 살아 있었지만, 실제 프로젝트는 여러 파일과 외부 라이브러리가 함께 동작합니다. 이번 글은 — Go의 코드 조직 시스템을 정리합니다.

두 가지 단위 — 패키지와 모듈 #

패키지 (package)모듈 (module)
단위한 폴더여러 폴더의 묶음
정의package 이름 선언go.mod 파일
비유클래스/네임스페이스프로젝트/라이브러리

한 폴더 = 한 패키지, 여러 폴더 + go.mod = 한 모듈. 모듈이 더 큰 단위입니다.

패키지 — 한 폴더가 한 단위 #

같은 폴더의 파일들은 모두 같은 패키지에 속해야 합니다.

폴더 구조
calculator/
├── add.go         (package calculator)
├── multiply.go    (package calculator)
└── helper.go      (package calculator)

모두 package calculator로 시작해야 합니다. 다른 패키지 이름이 섞이면 컴파일 에러.

main 패키지 — 실행 가능한 프로그램 #

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
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/myapp

go.mod가 만들어집니다.

go.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에서 다른 패키지 사용:

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

이 명령이:

  1. 패키지를 다운로드
  2. go.mod에 의존성 추가
  3. go.sum에 체크섬 기록
go.mod 갱신
module github.com/curtis/myapp

go 1.22

require github.com/google/uuid v1.6.0

go.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.0

semver(시맨틱 버저닝) — v메이저.마이너.패치. Go 모듈 시스템은 다음 규칙을 따릅니다.

  • 메이저 0 — 안정 보장 없음 (실험)
  • 메이저 1+ — 호환 보장 (마이너/패치 업데이트는 호환)
  • 메이저 2+ — import path에 /v2, /v3 같은 suffix 필수
v2 이상의 import
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 패키지 #

internal 폴더
myapp/
├── go.mod
├── main.go
├── api/
│   └── handler.go        (package api)
└── internal/
    └── auth/
        └── login.go      (package auth)

internal/ 폴더 안의 패키지는 — 그 모듈 또는 그 모듈의 자식만 import가능. 외부 모듈은 못 가져옵니다.

라이브러리를 만들 때 — 노출 API와 구현 디테일을 분리하는 표준 패턴입니다.

패키지 init 함수 #

각 패키지는 — 처음 import 될 때 자동 실행되는 init() 함수를 가질 수 있습니다.

init
package config

import "log"

var Settings map[string]string

func init() {
	Settings = loadFromFile()
	log.Println("config로드됨")
}

부수효과(설정 로드, 드라이버 등록)에 쓰입니다. 한 패키지에 여러 init도 가능합니다.

남용은 피하세요 — 명시적인 초기화 함수(Init())가 보통 더 명확합니다.

vendor 폴더 — 옵션 #

go mod vendor 명령으로 의존성을 프로젝트 안에 복사할 수 있습니다.

vendor
go mod vendor

vendor/ 폴더에 모든 의존성 코드가 들어옵니다. 빌드 시 그곳에서 가져옵니다.

장점: 외부 다운로드 없이 빌드 가능 (오프라인, 사설 네트워크에서). 단점: 저장소가 커짐.

요즘은 go.mod + 로컬 캐시($GOPATH/pkg/mod)로 충분해서 vendor는 잘 안 쓰는 추세입니다.

replace — 로컬 경로로 대체 #

라이브러리 개발 중 — 실제 의존성을 로컬 경로로 임시 대체.

go.mod
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 폴더 만들기

가벼운 가이드 — 패키지 디자인 #

큰 디자인 가이드를 모두 정리하긴 어렵지만, 자주 도움 되는 것들을 추렸습니다.

  1. 패키지 이름은 짧고 소문자auth, db, config
  2. 패키지가 여러 일을 하면 잘게 쪼개기
  3. 순환 의존(A → B, B → A)은 컴파일 에러 — 디자인이 꼬였다는 신호
  4. 외부 노출은 최소화 — 가능한 소문자 시작
  5. util, common 같은 잡동사니 패키지는 피하기 — 의도가 안 보임

마무리 #

기초 시리즈 7편을 거치며 다룬 내용:

  1. 시작과 첫 프로그램 — 셋업과 Hello World (#1)
  2. 변수, 타입, 상수:=, iota, untyped const (#2)
  3. 제어 흐름 — for가 유일, switch fallthrough 없음 (#3)
  4. 함수, 다중 반환, errorif err != nil, defer (#4)
  5. 컬렉션 — slice의 동작 원리, map의 comma-ok (#5)
  6. 구조체와 메서드 — 값/포인터 리시버, 임베딩 (#6)
  7. 패키지와 모듈 — 가시성, go.mod, internal (이번 글)

여기까지 잡으면 작은 프로그램과 CLI 도구는 자신 있게 만들 수 있습니다. 다음 중급 시리즈에서는 — 인터페이스, 동시성(고루틴/채널), 컨텍스트, 표준 라이브러리 같은 Go의 진짜 강점들을 다룹니다.

X