고 고급 #7 코드 생성 — go generate와 stringer

5 분 소요

#6 프로파일링에서 측정 도구를 봤다면 — 이번엔 측정 결과로 자주 발견되는 상황입니다. reflect가 핫 패스에 있을 때 어떻게 빠르게 대처할 수 있는지 정리합니다.

Go의 답은 — 코드 생성. 런타임에 reflect로 분석하는 대신, 컴파일 타임에 미리 코드를 만들어 두는 방식입니다. 고 고급 시리즈의 마지막 글입니다.

go generate의 그림 #

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의 이름을 동적으로 추출하는 방식입니다. 하지만 차이가 있습니다.

비교reflectcode gen
속도~100x 느림일반 코드와 동일
컴파일 타임 검증없음있음
디버깅어려움쉬움
빌드 복잡도단순생성 단계 추가

핫 패스에서 reflect를 쓰던 부분을 — 컴파일 타임 정보로 풀 수 있다면 항상 더 좋습니다.

stringer — enum 표현 #

stringer 설치
go install golang.org/x/tools/cmd/stringer@latest
여러 enum
//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/mockmockgen이 — 인터페이스에서 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으로 — 테스트에서 동작 미리 정의 가능.

mock 사용
mockDB := mocks.NewMockDB(ctrl)
mockDB.EXPECT().Get("u1").Return(User{Name: "이도경"}, nil)

svc := NewService(mockDB)
svc.Greet("u1")

reflect로 동적 mock을 만들 수도 있지만 — 자동 생성이 IDE 자동완성, 컴파일 타입 체크, 디버깅 모두에서 우수.

직접 만들기 — text/template으로 #

자주 등장하는 패턴입니다.

generator.go
//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 — 다양한 분야
easyjson 사용 예
//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에 묶기 #

Makefile
.PHONY: gen
gen:
	go generate ./...

.PHONY: build
build: gen
	go build ./...

생성을 빌드 전 단계로. 단, 자동 생성 파일은 — 보통 git에 커밋해 둡니다(다른 개발자가 도구 없어도 빌드 가능하게).

CI에서 검증 #

생성 파일이 — 항상 최신인지 확인.

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 편을 정리하면:

  1. 동시성 패턴 — pipeline, fan-out/fan-in, semaphore
  2. 메모리 모델과 sync — Mutex, atomic, Once
  3. 제네릭 — type parameter, constraint
  4. reflect — 런타임 타입 다루기
  5. unsafe와 cgo — 안전 영역 밖
  6. 프로파일링 — pprof, benchmark, trace
  7. 코드 생성 (이 글)

기초/중급에서 본 도구들의 한계가 보이는 경우 들 — 동시성을 잘 짜는 패턴, 공유 메모리의 동기화, 제네릭으로 다형성 표현, 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편 마무리.

X