Backend
Wire를 활용한 Go 의존성 주입(DI) 구현
Google Wire로 컴파일 타임 DI를 구성하는 실전 패턴을 정리합니다.
Wire를 쓰는 이유
Wire는 리플렉션 기반 컨테이너가 아니라 코드 생성 기반 DI입니다.
장점:
- 컴파일 타임 타입 검증
- 런타임 오버헤드 없음
- 생성 코드가 명시적이라 디버깅이 쉬움
단점:
- 초기 설정과 학습 비용
- 그래프가 크면 생성 코드 추적이 번거로울 수 있음
기본 구조
func NewConfig() *Config { ... }
func NewDB(cfg *Config) (*sql.DB, error) { ... }
func NewRepo(db *sql.DB) *Repo { ... }
func NewService(r *Repo) *Service { ... }
//go:build wireinject
func InitializeService() (*Service, error) {
wire.Build(
NewConfig,
NewDB,
NewRepo,
NewService,
)
return nil, nil
}
wire 실행 후 wire_gen.go가 생성됩니다.
Provider Set으로 모듈화
var InfraSet = wire.NewSet(NewConfig, NewDB, NewLogger)
var RepoSet = wire.NewSet(NewRepo)
var ServiceSet = wire.NewSet(NewService)
조립 루트:
func InitializeApp() (*App, error) {
wire.Build(InfraSet, RepoSet, ServiceSet, NewApp)
return nil, nil
}
인터페이스 바인딩
type UserRepo interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type userRepo struct { ... }
var RepoSet = wire.NewSet(
NewUserRepo,
wire.Bind(new(UserRepo), new(*userRepo)),
)
운영에서 자주 겪는 문제
- 빌드 태그(
wireinject) 누락으로 생성 파일 충돌 - Provider 함수가 숨은 사이드이펙트를 가져 테스트 격리 깨짐
- 생성 코드 커밋 정책이 팀마다 달라 CI 불일치
추천 규칙
- DI 조립 코드는
internal/app또는cmd/*/wire.go에 집중 - Provider는 생성 책임만, 환경 검증/실행 로직 분리
- CI에서
wire재실행 후 diff 검사
요약
Wire의 가치는 "자동 주입"이 아니라 명시적인 의존성 그래프를 안전하게 유지하는 데 있습니다. 규칙만 잘 잡으면 규모가 커질수록 수동 DI보다 유지보수가 쉬워집니다.
다음 읽기
이 생각이 이어지는 방향
읽은 뒤의 대화
읽은 뒤의 생각을 이어갑니다
질문, 반론, 조용한 후속 메모를 이 글 아래에 남길 수 있습니다.