Wire
wire 는 구글이 오픈소스로 공개한 의존성 주입 도구로, 의존성 주입이라는 개념은 Java 의 Spring 프레임워크에서 매우 인기가 있습니다. Go 에도 Uber 가 오픈소스로 공개한 dig 와 같은 의존성 주입 라이브러리가 있습니다. 그러나 wire 의 의존성 주입 철학은 언어의 리플렉션 메커니즘을 기반으로 하지 않습니다. 엄밀히 말하면 wire 는 코드 생성기이며 의존성 주입 철학은 사용법에体现됩니다. 문제가 있으면 코드 생성 중에 찾을 수 있습니다.
저장소 주소: google/wire: Compile-time Dependency Injection for Go (github.com)
문서 주소: wire/docs/guide.md at main · google/wire (github.com)
설치
코드 생성 도구 설치
go install github.com/google/wire/cmd/wire@latest소스 코드 의존성 설치
go get github.com/google/wire입문
wire 에서 의존성 주입은 두 가지 요소에 기반합니다. provider와 injector입니다.
provider는 개발자가 생성자를 제공할 수 있으며 다음과 같습니다. Provider 는 반드시 외부에 노출되어야 합니다.
package foobarbaz
type Foo struct {
X int
}
// Foo 생성
func ProvideFoo() Foo {
return Foo{X: 42}
}매개변수 포함
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}매개변수와 반환값 포함 가능
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}provider 를 조합할 수도 있습니다.
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire 는 provider 의 반환값에 대해 다음과 같은 규정이 있습니다.
- 첫 번째 반환값은 provider 가 제공하는 값입니다.
- 두 번째 반환값은
func() | error여야 합니다. - 세 번째 반환값은 두 번째 반환값이
func이면 세 번째 반환값은error여야 합니다.
injector는 wire 가 생성한 함수로, 지정된 순서대로 provider 를 호출하는 역할을 합니다. injector 의 시그니처는 개발자가 정의하며 wire 는 구체적인 함수 본문을 생성합니다. wire.Build를 호출하여 선언하며 이 선언은 호출되어서는 안 되며 컴파일되어서도 안 됩니다.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// 정의된 injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}그런 다음 실행
wirewire_gen.go 가 생성되며 내용은 다음과 같습니다.
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//+build !wireinject
package main
import (
"example.com/foobarbaz"
)
// 실제 생성된 injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
foo := foobarbaz.ProvideFoo()
bar := foobarbaz.ProvideBar(foo)
baz, err := foobarbaz.ProvideBaz(ctx, bar)
if err != nil {
return foobarbaz.Baz{}, err
}
return baz, nil
}생성된 코드는 wire 에 거의 의존하지 않으며 wire 가 없어도 정상적으로 작동할 수 있으며 이후 go generate 를 실행하여 다시 생성할 수 있습니다. 이후 개발자는 실제로 생성된 injector 를 호출하여 해당 매개변수를 전달하여 의존성 주입을 완료합니다. 전체 과정의 코드가 매우 간단하지 않나요? 마치 몇 개의 생성자를 제공하고 생성자를 호출하는 함수를 생성한 다음 이 함수를 호출하여 매개변수를 전달하는 것 같습니다. 특별히 복잡한 일은 하지 않은 것 같습니다. 수작업으로도 작성할 수 있습니다. 맞습니다. wire 가 하는 일은 이런 간단한 일이며 수작업에서 자동 생성으로 바뀌었을 뿐입니다. wire 의 철학에 따르면 의존성 주입은 본래 이렇게 간단한 일이어야 하며 복잡하게 만들어서는 안 됩니다.
예제
다음은 사례를 통해 이해를 깊게 하는 예제로, 이는 app 을 초기화하는 예제입니다.
HttpServer 의 provider 는 net.Addr 매개변수를 받으며 포인터와 error 를 반환합니다.
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}아래의 MysqlClient 와 System 의 provider 도 동일합니다.
var DataBaseProviderSet = wire.NewSet(NewMysqlClient)
type MysqlClient struct {
}
var SystemSet = wire.NewSet(NewApp)
type System struct {
server *HttpServer
data *MysqlClient
}
func (s *System) Run() {
log.Printf("app run on %s", s.server.String())
}
func NewApp(server *HttpServer, data *MysqlClient) (System, error) {
return System{server: server, data: data}, nil
}provider 정의가 완료되면 injector 를 정의해야 하며, wire.go 파일을 새로 만들어 정의하는 것이 좋습니다.
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// injector 정의
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// 순서대로 provider 호출
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject 는 컴파일 시 이 injector 를 무시하기 위한 것입니다. 그런 다음 다음 명령을 실행하며 다음과 같은 출력이 있으면 성공적으로 생성된 것입니다.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go생성된 코드는 다음과 같습니다.
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"net"
)
// Injectors from wire.go:
// injector 정의
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
httpServer, err := NewHttpserver(serverAddr)
if err != nil {
return System{}, err
}
mysqlClient, err := NewMysqlClient(dataAddr)
if err != nil {
return System{}, err
}
system, err := NewApp(httpServer, mysqlClient)
if err != nil {
return System{}, err
}
return system, nil
}로직이 매우 명확하고 호출 순서도 올바릅니다. 마지막으로 생성된 injector 를 통해 app 을 시작합니다.
package main
import (
"github.com/google/wire"
"log"
"net"
"net/netip"
)
func main() {
server, err := initSystemServer(
net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8080")),
"mysql:localhost:3306/test")
if err != nil {
panic(err)
}
server.Run()
}최종 출력은 다음과 같습니다.
2023/08/01 19:20:48 app run on 127.0.0.1:8080이는 매우 간단한 사용 사례입니다.
고급 사용법
인터페이스 바인딩
의존성 주입 시 구체적인 구현을 인터페이스에 주입할 때가 있습니다. wire 는 의존성 주입 시 타입 매칭을 기반으로 합니다.
type Fooer interface {
Foo() string
}
type MyFooer string
func (b *MyFooer) Foo() string {
return string(*b)
}
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}
type Bar string
func provideBar(f Fooer) string {
// f will be a *MyFooer.
return f.Foo()
}provider provideBar 의 매개변수는 인터페이스 타입이며 실제로는 *MyFooer 입니다. 코드 생성 시 provider 가 올바르게 매칭되도록 두 타입을 바인딩할 수 있습니다.
첫 번째 매개변수는 구체적인 인터페이스 포인터 타입이고 두 번째는 구체적인 구현의 포인터 타입입니다.
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)값 바인딩
wire.Build를 사용할 때 provider 가 값을 제공하지 않아도 되며 wire.Value 를 사용하여 구체적인 값을 제공할 수 있습니다. wire.Value 는 값을 생성하기 위한 표현식을 지원하며 이 표현식은 코드 생성 시 injector 에 복사됩니다.
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}생성된 injector
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)인터페이스 타입의 값을 바인딩하려면 wire.InterfaceValue 를 사용할 수 있습니다.
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}구조체 생성
providerset 에서 wire.Struct 를 사용하여 다른 provider 의 반환값으로 지정된 타입의 구조체를 생성할 수 있습니다.
첫 번째 매개변수는 구조체 포인터 타입을 전달해야 하며 이후는 필드명입니다.
func Struct(structType interface{}, fieldNames ...string) StructProvider예제는 다음과 같습니다.
type Foo int
type Bar int
func ProvideFoo() Foo {/* ... */}
func ProvideBar() Bar {/* ... */}
type FooBar struct {
MyFoo Foo
MyBar Bar
}
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "MyFoo", "MyBar"))
func injectFooBar() FoodBar {
wire.Build(Set)
}생성된 injector 는 다음과 같을 수 있습니다.
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}모든 필드를 채우려면 * 를 사용할 수 있습니다.
wire.Struct(new(FooBar), "*")기본적으로 구조체 타입을 생성하며 포인터 타입을 생성하려면 injector 시그니처의 반환값을 수정할 수 있습니다.
func injectFooBar() *FoodBar {
wire.Build(Set)
}필드를 무시하려면 tag 를 추가할 수 있습니다.
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Cleanup
provider 가 생성한 값이 사용 후 마무리 작업 (예: 파일 닫기) 이 필요한 경우 provider 는 클로저를 반환하여 이러한 작업을 수행할 수 있습니다. injector 는 이 cleanup 함수를 호출하지 않으며 언제 호출할지는 injector 의 호출자에게 맡깁니다.
type Data struct {
// TODO wrapped database client
}
// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{}, cleanup, nil
}실제로 생성된 코드는 다음과 같을 수 있습니다.
func wireApp(confData *conf.Data, logger log.Logger) (func(), error) {
dataData, cleanup, err := data.NewData(confData, logger)
if err != nil {
return nil, nil, err
}
// inject data
// ...
return app, func() {
cleanup()
}, nil
}타입 중복
provider 의 입력 매개변수는 타입이 중복되지 않는 것이 좋으며 특히 일부 기본 타입의 경우
type FooBar struct {
foo string
bar string
}
func NewFooBar(foo string, bar string) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
func InitializeFooBar(a string, b string) FooBar {
panic(wire.Build(NewFooBar))
}이 경우 코드 생성 시 오류가 발생합니다.
provider has multiple parameters of type stringwire 는 이러한 매개변수를 어떻게 주입할지 구분할 수 없으므로 충돌을 방지하기 위해 타입 별명을 사용할 수 있습니다.
