고 고급 #7 코드 생성 — go generate와 stringer
#6 프로파일링에서 측정 도구를 봤다면 — 이번엔 측정 결과로 자주 발견되는 상황입니다. reflect가 핫 패스에 있을 때 어떻게 빠르게 대처할 수 있는지 정리합니다.
Go의 답은 — 코드 생성. 런타임에 reflect로 분석하는 대신, 컴파일 타임에 미리 코드를 만들어 두는 방식입니다. 고 고급 시리즈의 마지막 글입니다.
go generate의 그림
#
go generate는 — 소스 안의 특별한 주석을 만나면 명령을 실행하는 도구입니다.
//go:generate stringer -type=Status
package main
type Status int
const (
Pending Status = iota
Active
Closed
)go generate ./...이 명령이 — 위 주석을 발견하고 stringer -type=Status를 실행. 그 도구가 새 파일을 생성합니다.
결과 — 자동 생성 파일 #
stringer가 만들어 낸 status_string.go:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
package main
func (i Status) String() string {
switch i {
case Pending:
return "Pending"
case Active:
return "Active"
case Closed:
return "Closed"
}
return "Status(...)"
}이제 Status가 — fmt.Stringer를 만족. fmt.Println(s) 면 “Active” 같은 사람 읽기 좋은 문자열.
왜 reflect보다 좋은가? #
같은 일을 reflect로도 할 수 있습니다. enum의 이름을 동적으로 추출하는 방식입니다. 하지만 차이가 있습니다.
| 비교 | reflect | code gen |
|---|---|---|
| 속도 | ~100x 느림 | 일반 코드와 동일 |
| 컴파일 타임 검증 | 없음 | 있음 |
| 디버깅 | 어려움 | 쉬움 |
| 빌드 복잡도 | 단순 | 생성 단계 추가 |
핫 패스에서 reflect를 쓰던 부분을 — 컴파일 타임 정보로 풀 수 있다면 항상 더 좋습니다.
stringer — enum 표현 #
go install golang.org/x/tools/cmd/stringer@latest//go:generate stringer -type=Status,Priority
type Status int
const (
Pending Status = iota
Active
)
type Priority int
const (
Low Priority = iota
High
)생성 파일: status_string.go, priority_string.go. enum이 추가될 때마다 — go generate 한 번이면 새로 만들어집니다.
mockgen — 인터페이스 모킹 #
go.uber.org/mock의 mockgen이 — 인터페이스에서 mock을 자동 생성.
//go:generate mockgen -source=db.go -destination=mock_db.go -package=mocks
type DB interface {
Get(id string) (User, error)
Save(u User) error
}생성된 mock으로 — 테스트에서 동작 미리 정의 가능.
mockDB := mocks.NewMockDB(ctrl)
mockDB.EXPECT().Get("u1").Return(User{Name: "이도경"}, nil)
svc := NewService(mockDB)
svc.Greet("u1")reflect로 동적 mock을 만들 수도 있지만 — 자동 생성이 IDE 자동완성, 컴파일 타입 체크, 디버깅 모두에서 우수.
직접 만들기 — text/template으로
#
자주 등장하는 패턴입니다.
//go:build ignore
package main
import (
"os"
"text/template"
)
const tmpl = `// DO NOT EDIT.
package main
func {{.Name}}Greet() string {
return "Hello, {{.Name}}!"
}
`
func main() {
t := template.Must(template.New("g").Parse(tmpl))
f, _ := os.Create("greet_gen.go")
t.Execute(f, map[string]string{"Name": "World"})
}//go:generate go run generator.go//go:build ignore 태그가 있어서 — 일반 빌드에선 제외, go run generator.go로만 실행.
패턴 — 직렬화 가속 #
큰 프로젝트에서 자주 보이는 경우입니다. encoding/json의 reflect 비용을 — 코드 생성으로 회피.
도구들:
mailru/easyjson— struct → 전용 마샬러 코드pquerna/ffjson— 비슷한 컨셉mvdan/gogenerate,vektra/mockery— 다양한 분야
//go:generate easyjson -all user.go
//easyjson:json
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}생성된 user_easyjson.go — User 타입 전용의 MarshalJSON / UnmarshalJSON. reflect 없이 직접 byte를 다뤄서 빠릅니다.
벤치마크 결과는 보통 — encoding/json보다 2~5 배 빠른 경우도 있습니다. 다만 — 빌드 단계가 늘고 자동 생성 코드가 PR에 섞이는 trade-off.
protobuf / gRPC — 자동 생성의 정점 #
protoc --go_out=. user.proto.proto 파일에서 — Go struct, 마샬러, gRPC 서버/클라이언트 코드 모두 자동 생성. 실전에서 보는 가장 큰 코드 생성 사례.
user.pb.go ← struct + Marshal/Unmarshal
user_grpc.pb.go ← gRPC 서버/클라이언트수만 줄의 코드가 — .proto 한 파일에서. 이 파이프라인이 마이크로서비스의 표준 도구.
실전 워크플로우 #
Makefile에 묶기 #
.PHONY: gen
gen:
go generate ./...
.PHONY: build
build: gen
go build ./...
생성을 빌드 전 단계로. 단, 자동 생성 파일은 — 보통 git에 커밋해 둡니다(다른 개발자가 도구 없어도 빌드 가능하게).
CI에서 검증 #
생성 파일이 — 항상 최신인지 확인.
go generate ./...
git diff --exit-code # 생성 후 변경이 있으면 실패PR에 새 자동 생성 파일이 빠져 있으면 CI가 잡아 줍니다.
어디까지 자동 생성할 것인가 #
회피해야 할 경우:
- 로직이 자주 바뀌는 코드 — 매번 생성 단계 거치면 부담
- PR 리뷰가 어려운 큰 생성 파일 — 진짜 변경이 묻힘
- 외부 도구 의존이 부담스러운 작은 프로젝트
자연스러운 경우:
- enum의 String 메서드, JSON 마샬러 — 정적 정보 기반
- 인터페이스 mock
- protobuf / OpenAPI 같은 스펙 → 코드 변환
규칙은 단순 — 같은 패턴을 손으로 5번 이상 쓰게 되면 자동 생성 검토 시점.
함정 — 생성 파일을 직접 수정 #
// Code generated by ...; DO NOT EDIT.이 헤더가 보이면 — 절대 직접 편집 금지. 다음 generate에서 덮어 써집니다. 정말 수정이 필요하면 — 생성 도구 자체 또는 템플릿을 수정.
함정 — 생성 도구 버전 고정 #
//go:build tools
// +build tools
package tools
import (
_ "golang.org/x/tools/cmd/stringer"
_ "go.uber.org/mock/mockgen"
)tools.go에 — 사용 도구를 import (build tag로 일반 빌드 제외). go.mod가 버전을 고정. 팀 전체가 같은 도구 버전을 보장.
시리즈 마무리 — 고 고급 7편을 돌아보며 #
7 편을 정리하면:
- 동시성 패턴 — pipeline, fan-out/fan-in, semaphore
- 메모리 모델과 sync — Mutex, atomic, Once
- 제네릭 — type parameter, constraint
- reflect — 런타임 타입 다루기
- unsafe와 cgo — 안전 영역 밖
- 프로파일링 — pprof, benchmark, trace
- 코드 생성 (이 글)
기초/중급에서 본 도구들의 한계가 보이는 경우 들 — 동시성을 잘 짜는 패턴, 공유 메모리의 동기화, 제네릭으로 다형성 표현, reflect/unsafe/cgo로 경계 통과, 측정과 자동화. 이 도구들이 다 같이 등장하는 경우는 보통 프레임워크나 라이브러리 코드에서입니다.
다음 단계 #
다음 시리즈(#1 첫 HTTP 서버)는 Go 실전 6편. 지금까지 배운 도구를 — 실제 백엔드를 짜며 어떻게 조립하는지 정리합니다. net/http, ServeMux, JSON, DB 연동, 미들웨어, 테스트와 배포까지 정리합니다.
마무리 #
이번 글에서 정리한 내용:
//go:generate+go generate ./...— 컴파일 타임 코드 생성 표준- stringer — enum String 메서드
- mockgen — 인터페이스 mock
- easyjson / protoc — 직렬화 가속, RPC 코드
- 자동 생성 파일은 — git에 커밋, 직접 편집 금지, CI에서 최신 여부 검증
- 도구 버전은 —
tools.go로 고정 - reflect가 핫 패스에 있다면 — 코드 생성을 검토
이로써 고 고급 7편 마무리.