티스토리 뷰

언어/Golang

[Go]Uber-fx DI Framework 2편

꼬마우뇽이(원종운) 2022. 12. 11. 15:14

fx.Invoke

모든 의존성 컨테이너가 초기화 된 후, 실행하고 싶은 함수나 메서드를 등록할 수 있도록 하여주는 fx.Option 입니다.

실행 과정 살펴보기

app.root 란 !?

app.root 는 최상위 모듈로서 아래에서 살펴볼 수 있듯이 fx.New 를 통해 내부적으로 생성되는 fx.App 을 가지고 있는 모듈로, 최상위 모듈입니다.

최상위 모듈도 생성된 fx.App 에 등록된 모든 모듈을 관리하는 app.modules 에 추가된 것을 확인할 수 있습니다.

모든 의존성 컨테이너가 초기화된 후 최종적으로 fx.App 에 등록된 모든 모듈의 fx.Invoke 를 실행합니다.

app.root.executeInvokes() 메서드 호출이 그 역할을 수행합니다.

 

 

잠깐 살펴보는 fx.module 구조체

fx.Module Option 내에 정의한 fx.Invoke , fx.Provide , fx.Modulefx.module 구조체로 관리되어지며, 모두 Slice 자료 구조로 관리되어짐을 알 수 있습니다.

 

executeInvokes() 메서드는 module 구조체의 메서드입니다.

 

위 그림의 상자를 보면 알 수 있듯이 두 개의 파트로 나누어 등록된 fx.Invoke 에 전달한 Invoke 함수(메서드)가 실행됩니다.

 

1번 상자는 자신의 모듈 하위에 등록된 모듈들에 대해서 executeInvokes() 메서드를 실행하는 것을 확인할 수 있습니다. 분석을 해보면 다음과 같습니다.


1. m.modules 는 slice 로 관리되어지기때문에 등록되는 모듈의 순서도 전체적인 Invoke 함수(메서드)의 실행 순서에 영향을 줍니다.


2. 다시 하위 모듈에 대해서 재귀적으로 executeInvokes() 메서드를 수행합니다.

    → 자신의 모듈 하위에 등록된 모듈에 대해서 Invoke 함수(메서드)를 먼저 실행한다는 것을 알 수 있습니다.


3. 자신의 하위 모듈에 대해서 모든 Invoke 함수(메서드) 실행이 완료되면, 자신의 모듈에 등록된 Invoke 함수(메서드)를 실행합니다.

 

 

실제로 실행되는 순서를 확인해봅시다.

package main

import (
    "fmt"
    "go.uber.org/fx"
)

func func1() { fmt.Println("test1") }
func func2() { fmt.Println("test2") }
func func3() { fmt.Println("test3") }
func func4() { fmt.Println("test4") }
func func5() { fmt.Println("test5") }
func func6() { fmt.Println("test6") }

func main() {
    fx.New(
        fx.Invoke(func1),
        fx.Module("module A",
            fx.Invoke(func2),
            fx.Invoke(func3),
        ),
        fx.Module("module B",
            fx.Invoke(func4),
            fx.Invoke(func5),
        ),
        fx.Invoke(func6),
    ).Run()
}

// 결과 
// "test2" -> "test3" -> "test4" -> "test5" -> "test1" -> "test6"

요약

 

fx.Annotate

생성자에 필요한 의존성 또는 생성자에서 반환하는 새로운 의존성에 대하여 추가적인 주석 정보를 추가할 수 있도록 하여주는 fx.Option 입니다.

 

 

왜 추가적인 정보를 의존성에 등록하는 것을 제공할까요?

 

UseCase 1. 구조체로 반환, 인터페이스로 주입

 

Go에서는 의존성 역전, 느슨한 결합 등을 목적으로 명시적으로 인터페이스를 구현하기 보다는 암시적으로 인터페이스를 구현합니다.

 

이것과 uber-fx 와 같은 의존성 주입 프레임워크와 만나면, 현재까지의 사용 방법으로는 의존성 주입이 어렵습니다. 한번 살펴보겠습니다.

 

package main

import (
    "fmt"
    "go.uber.org/fx"
)

type Test interface{ Hello() string }
type TestImpl struct{}

func (t TestImpl) Hello() string { return "hello" }
func NewTestImpl() *TestImpl     { return &TestImpl{} }

type App struct{ test Test }

func NewApp(test Test) *App      { return &App{test: test} }
func Invoke(app *App) interface{} { fmt.Println(app.test.Hello()); return nil }

func main() {
    fx.New(
        fx.Provide(NewTestImpl),
        fx.Provide(NewApp),
        fx.Invoke(Invoke),
    ).Run()
}

 

fx.Provide(NewTestImpl) 을 통해서 *TestImpl 타입의 의존성을 컨테이너에 등록하였습니다.

 

NewApp 가 호출되어질 때 필요한 의존성을 가져오게 되는데, 하지만 *TestImpl 타입이 Test 인터페이스를 구현하고 있고, 의존성이 등록되어있더라도 주입받지 못하여 다음과 같은 오류가 발생합니다.

 

→ fx에서도 혹시 주입받고자하는 Test 인터페이스 타입의 의존성이 *TestImpl 구조체냐라고 묻습니다.

missing type: - main.App (did you mean to use *main.App?)

 

방법 1. 보일러 플레이트 작성

인터페이스를 생성자의 인자로 받는 경우, 직접 넣어주는 하나의 생성자를 추가로 만듭니다.

package main

import (
    "fmt"
    "go.uber.org/fx"
)

type Test interface{ Hello() string }
type TestImpl struct{}

func (t TestImpl) Hello() string { return "hello" }
func NewTestImpl() *TestImpl     { return &TestImpl{} }

type App struct{ test Test }

func NewApp(test Test) *App       { return &App{test: test} }
func Invoke(app *App) { fmt.Println(app.test.Hello()) }

func newApp(testImpl *TestImpl) *App {
    return NewApp(testImpl)
}

func main() {
    fx.New(
        fx.Provide(NewTestImpl),
        fx.Provide(newApp),
        fx.Invoke(Invoke),
    ).Run()
}

 

구조체는 주입받을 수 있기때문에 *App 의존성을 등록하기 위하여 newApp 생성자를 하나 더 만들어, 구조체로 의존성을 주입받고, 직접 의존성을 넣어주는 형태로 가능합니다.

 

장점

 

의존성을 인터페이스 형태로 받아서 사용할 수 있습니다.

실제 구현체가 인터페이스를 구현하고 있는지 컴파일 타임에 확인할 수 있습니다.

 

단점

 

생성자의 인자로 인터페이스를 받고 싶은 경우, 위와 같은 추가적인 생성자가 필요하며 보일러 플레이트가 늘어나게 됩니다.

방법 2. fx.Annotate + fx.As 사용

위와 같이 간단한 작업인데, 계속 반복되는 보일러 플레이트 작업을 추상화하여 주는 기능을 fx 에서는 제공하고 있습니다.

 

fx.As 가 무엇일까요? 언제 어떻게 쓰면 좋을까요?

 

fx.As

fx.Asfx.Annotation 중 하나이며, 함수의 결과 타입을 다른 형태의 타입으로 의존성으로 제공할 수 있도록하여줍니다.

 

// go.uber.org/fx/annotated.go - line 536
// As is an Annotation that annotates the result of a function (i.e. a
// constructor) to be provided as another interface.
//
// For example, the following code specifies that the return type of
// bytes.NewBuffer (bytes.Buffer) should be provided as io.Writer type:
//
//    fx.Provide(
//      fx.Annotate(bytes.NewBuffer(...), fx.As(new(io.Writer)))
//    )
//
// In other words, the code above is equivalent to:
//
//    fx.Provide(func() io.Writer {
//      return bytes.NewBuffer()
//      // provides io.Writer instead of *bytes.Buffer
//    })

 

위 공식 문서의 설명과 예제를 보면, 위에서 작성했던 보일러 플레이트와 동일한 기능을 수행하여 주는 것을 확인할 수 있습니다.

 

위에서 작성했던 코드를 fx.Annotatefx.As 를 적용해보면 다음과 같이 수정할 수 있습니다.

 

package main

import (
    "fmt"
    "go.uber.org/fx"
)

type Test interface{ Hello() string }
type TestImpl struct{}

func (t TestImpl) Hello() string { return "hello" }
func NewTestImpl() *TestImpl     { return &TestImpl{} }

type App struct{ test Test }

func NewApp(test Test) *App { return &App{test: test} }
func Invoke(app *App)       { fmt.Println(app.test.Hello()) }

func main() {
    fx.New(
        fx.Provide(fx.Annotate(NewTestImpl, fx.As(new(Test)))),
        fx.Provide(NewApp),
        fx.Invoke(Invoke),
    ).Run()
}

 

fx.Annotate(NewTestImpl, fx.As(new(Test))) 를 해석해보면 다음과 같습니다.

 

NewTestImpl 생성자가 반환하는 값 중 첫 반환값Test 인터페이스의 타입으로 제공할 수 있도록 fx.Annotation 을 적용합니다.

 

 

NewApp 생성자에서 Test 인터페이스 타입의 의존성이 필요할 경우, 의존성 주입할 때 Test 타입 인터페이스로 제공될 수 있는 의존성이 있는지 검사합니다.

 

*TestImpl 는 fx.As 를 이용하여 Test 인터페이스 타입으로 의존성이 제공되어질 수 있는 상황합니다.

 

그러면 fx는 해당 의존성이 진짜 Test 인터페이스 타입을 구현하고 있는지 확인한 후 주입하여 줍니다.

런타임에서 검사가 이루어지기때문에, 컴파일 타임에서 구현체가 인터페이스를 구현하고 있는지 알 방법이 없습니다.

→ 그렇기때문에 컴파일 타임에서 체크를 할 수 있도록 fx를 사용할 경우 다음과 같은 코드를 작성하는 것을 볼 수 있습니다.

 

var _ Test = (*TestImpl)(nil)

 

만약 *TestImpl 이 Test 인터페이스를 모두 구현하지 않았다면, 컴파일 타임에 아래와 같이 오류를 확인할 수 있습니다.

*TestImpl does not implement Test (missing Hello method)

 

주의

fx.As 내에 정의되는 다른 타입이 적용되는 순서는 생성자에서 반환하는 의존성의 순서와 동일합니다.

위 코드에서 fx.As 의 1번째 타입은 NewTestImpl 생성자에서 반환되는 의존성 중 1번째 의존성에 대하여 적용됩니다.

 

 

만약 생성자에서 반환하는 의존성이 2개 이상이라면 ?

순서에 맞춰서 적용하여 주면 됩니다.

 

package main

import (
	"fmt"
	"go.uber.org/fx"
)

type Test interface{ Hello() string }
type TestImpl struct{ Key string }

func (t TestImpl) Hello() string { return "test1" }

type Test2 interface{ Test() string }
type Test2Impl struct{ Key string }

func (t Test2Impl) Test() string { return "test2" }

func NewTestImpl() (*TestImpl, *Test2Impl) {
	return &TestImpl{Key: "test1"}, &Test2Impl{"test2"}
}

type App struct {
	testA Test
	testB Test2
}

func NewApp(testA Test, testB Test2) *App {
	return &App{testA: testA, testB: testB}

}

func Invoke(app *App) {
	fmt.Println(app.testA.Hello())
	fmt.Println(app.testB.Test())
}

var _ Test = (*TestImpl)(nil)

func main() {
	fx.New(
		fx.Provide(fx.Annotate(NewTestImpl, fx.As(new(Test), new(Test2)))),
		fx.Provide(NewApp),
		fx.Invoke(Invoke),
	).Run()
}

// 결과
// "test1"
// "test2"

 

적용 순서

 

fx.Annotate 는 여러 Annotation을 적용할 수 있도록 되어있습니다. fx.As 와 같이, 다른 Annotation도 위와 같은 순서대로 적용됩니다.

 

다만 Annotation에 따라 생성자에 주입되는 의존성에 Annotation을 적용하여주는 Annotation이 있고, 위와 같이 생성자가 반환하는 의존성에 Annotation을 적용하여주는 Annotation도 있습니다.

 
fx 에서는 다음과 같은 Annotation을 지원하여줍니다.
 
fx.asAnnotation (fx.As)
fx.resultTags (fx.ResultTags)
fx.paramTags (fx.ParamTags)
fx.lifecycleHookAnnotation (fx.Lifecyle)
 

 

 
다음 시간에는 생성자가 필요한 의존성을 지정하는 데 도움을 주는Tag를 지정할 수 있는 ParamTags Annotation 과 생성자가 반환하는 의존성에 Tag를 남길 수 있는 ResultTags Annotation을 살펴보겠습니다.
 
이를 통해 특정 인터페이스를 구현하는 여러 구조체를 모두 주입 받거나, 그 구현체 중 특정 구현체를 주입 받을수도 있습니다.

'언어 > Golang' 카테고리의 다른 글

[Go]Uber-fx DI Framework 1편  (1) 2022.12.04
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함