티스토리 뷰
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.Module 는 fx.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.As
는 fx.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.Annotate
와 fx.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도 있습니다.
'언어 > Golang' 카테고리의 다른 글
[Go]Uber-fx DI Framework 1편 (1) | 2022.12.04 |
---|
- Total
- Today
- Yesterday
- dfs
- 스트림
- k8s
- Java
- 스택
- BFS
- 정렬
- 프로그래머스
- 오늘의집
- 알고리즘
- 비트연산
- 우선순위큐
- dsu
- TDD
- kotlin
- 코딩인터뷰
- 연결리스트
- 구현
- JPA
- 해쉬
- 문자열
- 쓰레드
- Uber
- 탐욕법
- 카카오
- set
- 회고
- sql
- dp
- 코드 스니펫
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |