Go에서 Dependency Injection, 필드 태그로 축소
Source: Dev.to
핵심 아이디어
모든 의존성 주입은 구조체 필드 태그를 사용해 선언한다.
그 외는 없다.
- 제공자 집합이 없음.
- DSL이 없음.
- 런타임 리플렉션이 없음.
컨테이너는 무엇이 주입되는지를 선언한다.
제공자는 어떻게 값이 생성되는지를 선언한다.
컨테이너
컨테이너는 단순히 구조체이다. inject 태그가 붙은 필드는 인젝터가 관리한다.
type Container struct {
UserService service.UserService `inject:""`
}
inject 태그는 의도적으로 최소화되어 있다:
- 마커 역할만 한다.
- 기본적으로 별도 설정을 담고 있지 않다.
- “이 필드는 인젝터에 의해 주입된다”는 의미일 뿐이다.
제공자
제공자는 값을 반환하는 모든 최상위 함수이다.
func NewUserService(db infra.Database) service.UserService {
return &userService{DB: db}
}
규칙은 간단하다:
- 함수는 최상위이어야 한다(리시버가 없어야 함).
- 매개변수는 의존성이다.
- 반환값이 제공되는 타입이다.
인젝터는 정적 분석을 통해 제공자를 자동으로 발견한다.
생성된 코드
제너레이터를 실행한다:
injector generate ./...
그 결과는 순수 Go 코드이다:
func NewContainer() *Container {
cfg := NewDatabaseConfig()
db := NewDatabase(cfg)
user := NewUserService(db)
return &Container{
UserService: user,
}
}
런타임 매직은 없다. 생성된 코드는 읽기 쉽고, 디버깅 가능하며, 타입‑안전하다.
기본적으로 인터페이스‑우선
인젝터는 인터페이스와 자연스럽게 동작한다.
type UserService interface {
Register(name, password string) error
}
func NewUserService(db infra.Database) UserService {
return &userService{DB: db}
}
컨테이너는 인터페이스만 노출한다. 구체 구현은 비공개로 유지되어, DI‑전용 추상화를 추가하지 않으면서 애플리케이션 경계를 깔끔하게 유지한다.
여러 제공자 처리
동일한 타입을 반환하는 제공자가 여러 개 있을 경우, 인젝터는 명시적인 선택을 요구한다.
type Container struct {
_ config.DatabaseConfig `inject:"provider:NewPrimaryDatabaseConfig"`
UserService service.UserService `inject`
}
빈(_) 필드:
- 의존성을 노출하지 않는다.
- 제공자 오버라이드를 선언한다.
- 컨테이너 전체에 전역 적용된다.
제공자 선택은 중앙 집중식이며 명시적이다.
왜 필드 태그인가?
Go 구조체는 이미 의존성 목록을 나타낸다. 추가적인 설정 레이어를 넣으면 DI를 이해하기가 더 어려워진다.
DI 선언을 필드 태그에만 제한함으로써:
- 의존성을 한눈에 볼 수 있다.
- 설정이 로컬에 머문다.
- 정신 모델이 작아진다.
DI는 인프라스트럭처여야 하며, 코드베이스의 핵심이 되어서는 안 된다.
현황
인젝터는 의도적으로 작고 의견이 강하게 반영되어 있다. 현재 목표는 기능의 폭이 아니라 명확성이다:
- 마커‑기반 컨테이너.
- 제공자‑기반 해석.
- 컴파일‑타임 의존성 그래프.
향후 아이디어도 존재하지만, 실제 사용 사례가 나타날 때까지 기다릴 수 있다.
링크
GitHub:
피드백을 언제든 환영한다 — 특히 설계 트레이드‑오프와 엣지 케이스에 관한 의견을 기다린다.