Go · Dependency Injection

uber/fx로 시작하는
Go 의존성 주입

생성자를 등록하면 조립은 fx가 한다
— 타입으로 그래프를 만들고, 수명주기까지 관리하기

fx.Provide fx.Invoke fx.Module fxtest
fx가 자동으로 만드는 그래프 types only
config.New() ├─ database.New(cfg) │ ├─ NewMysqlArticleRepository(db) │ └─ NewMysqlAuthorRepository(db) │ └─ NewArticleUsecase(repo, authorRepo, timeout) │ └─ NewArticleHandler(echo, usecase) └─ NewEcho(cfg) └─ registerHooks(lifecycle, echo, cfg)
등록 순서는 상관없다. fx는 매개변수·반환 타입만 본다.
인사. 오늘은 uber/fx 한 바퀴. 오른쪽 트리가 fx가 자동으로 만들어주는 것이고, 우리는 생성자만 등록한다는 점을 먼저 각인시키고 시작한다.
문제why DI

main()이 조립 설명서가 되어간다

  • 생성자를 순서대로 직접 호출해야 한다
  • 매개변수 순서를 손으로 맞춘다
  • 새 서비스를 추가할 때마다 main()을 고친다
  • 의존성이 늘수록 이 코드는 급격히 복잡해진다
// 수동 DI: main()에서 직접 조립
cfg, _ := config.New()
db, _  := database.New(cfg)

authorRepo  := author.NewMysqlAuthorRepository(db)
articleRepo := article.NewMysqlArticleRepository(db)

timeout := time.Duration(cfg.Context.Timeout) * time.Second
usecase := article.NewArticleUsecase(articleRepo, authorRepo, timeout)

e := NewEcho()
article.NewArticleHandler(e, usecase)

e.Start(cfg.Server.Address)
여기서 진짜 비용 조립 순서라는 지식이 코드 여기저기에 흩어진다. 그 지식은 사실 타입 안에 이미 들어 있다.
"이 코드 익숙하시죠?"로 공감대부터. 순서를 틀리면 컴파일 에러가 나긴 하지만, 진짜 문제는 조립 지식이 main에 눌러앉는다는 점이라는 걸 짚는다.
해법uber/fx

fx는 타입만 보고 순서를 정한다

우리가 하는 일

생성자 함수를 등록만 한다. 순서는 신경 쓰지 않는다.

fx.Provide(
    config.New,
    database.New,
    NewArticleUsecase,
)
fx가 하는 일

각 생성자의 매개변수 타입반환 타입을 읽어 그래프를 구성하고, 올바른 순서로 호출한다.

New(cfg *config.Config) (*sql.DB, error)
     ↑ 필요            ↑ 제공
한 줄 요약 database.New*config.Config를 요구하므로, 그 타입을 반환하는 config.New먼저 호출된다. 순환 의존성은 앱 시작 시점에 에러로 알려준다.
go get go.uber.org/fx
"리플렉션으로 타입을 본다"는 게 핵심. 컴파일 타임 안전성을 일부 내주는 대신 조립 지식을 타입에 위임하는 거래라는 프레임으로 설명하면 뒤의 트레이드오프 이야기(30번)와 연결된다.
목차agenda

오늘 다루는 것

내용핵심 메서드
01fx 기초 — 등록·실행·수명주기Provide Invoke Supply Lifecycle
02확장 패턴 — 커진 앱을 정리하는 도구들Module Decorate Annotate Private
03테스트 전략fxtest Replace Populate
04정리 — 언제 쓰고 언제 안 쓰나선택 가이드

fx는 메서드가 많아 헷갈리기 쉽다. 다음 슬라이드의 지도를 먼저 펼쳐두고, 각 장에서 하나씩 실전 예제로 채워 나간다.

4장 구성. 시간이 모자라면 03(테스트)을 줄이고 01·02에 집중한다. 02의 group/Private은 실무에서 늦게 만나는 주제라 뒤로 밀어도 무방.
01

fx 기초

등록하고, 실행하고, 수명주기를 맡긴다. 이 네 가지 — Provide · Invoke · Supply · Lifecycle — 만 알아도 실전 앱 하나는 굴러간다.

여기가 오늘의 뿌리. 특히 Provide(lazy)와 Invoke(eager)의 차이는 뒤에서 계속 재등장하므로 시간을 아끼지 말 것.
지도cheat sheet

fx 메서드 한눈에 ① 기초

메서드분류역할
fx.New기초앱 컨테이너 생성 — 모든 Option을 모아 그래프를 만든다
fx.Provide기초lazy 등록. 반환 타입 기준으로 그래프에 올린다
fx.Invoke기초eager 실행. 부수 효과의 시작점
fx.Supply기초생성자 없이 이미 만든 값을 등록
fx.Lifecycle수명주기OnStart / OnStop 훅
오늘의 8할 이 다섯 개만 알아도 실전 앱 하나는 굴러간다. 다음 장의 확장·테스트 도구들은 앱이 커진 뒤에 필요해진다.
읽어 내려가지 말 것. "지금은 이 다섯 개만" 하고 넘어간다. 뒤에서 하나씩 예제로 채운다.
지도cheat sheet

fx 메서드 한눈에 ② 확장 · 테스트

메서드분류역할도입
fx.Module확장도메인별 그룹화v1.17+
fx.Decorate확장기존 의존성 래핑 (로깅·캐싱·메트릭)v1.18+
fx.Annotate확장생성자에 name / group / As 부여
fx.In / fx.Out확장파라미터·반환값을 구조체로 묶어 태그 매칭
fx.Private확장Module 내부 의존성 캡슐화v1.20+
fxtest.New테스트테스트 전용 앱
fx.Replace테스트기존 Provide를 Mock으로 교체
fx.Populate테스트내부 인스턴스를 외부 변수로 추출
버전 표기가 있는 셋(Module·Decorate·Private)은 실무에서 "왜 안 되지?" 하는 원인이 되곤 한다. go.mod 버전을 확인하라고 한마디 덧붙인다.
구조fx.Option

거의 모든 것이 fx.Option을 반환한다

app := fx.New(          // ← Option 들을 모아 앱을 만든다
    fx.Provide(...),   // fx.Option
    fx.Supply(...),    // fx.Option
    UserModule,        // fx.Option (fx.Module 이 반환)
    fx.Decorate(...),  // fx.Option
    fx.Invoke(...),    // fx.Option
)
① Option을 반환하는 것들

Provide · Invoke · Supply · Module · Decorate · Replace · Populate
fx.New의 인자로 들어간다

② Annotation을 반환하는 것들

Annotate · ResultTags · ParamTags
Provide · Supply 안에서 쓰인다

이 둘만 구분하면 fx API가 갑자기 단순해진다. 어디에 넣어야 할지 헷갈릴 일이 없다.
fx 문서를 처음 볼 때 가장 헷갈리는 지점. Option인지 Annotation인지만 구분되면 "이걸 어디에 넣지?" 하는 고민이 사라진다.
기초fx.Provide

Provide는 등록만 한다 — 실행은 미룬다

app := fxtest.New(t,
    // Provide: 생성자를 등록만 한다. 즉시 실행되지 않는다.
    fx.Provide(
        NewLogger,        // Logger 반환
        NewMysqlUserRepo, // UserRepository 반환 (Logger 필요)
        NewUserService,   // *UserService 반환 (UserRepository 필요)
    ),
    // Invoke: 여기서 비로소 그래프가 조립된다
    fx.Invoke(func(s *UserService) { svc = s }),
)
lazy 라는 뜻 등록된 생성자는 그 타입을 요구하는 쪽이 생길 때 비로소 호출된다. 아무도 안 찾으면 영영 호출되지 않는다.
그래서 순서 무관 위에서 NewUserService를 먼저 써도 결과는 같다. fx가 타입으로 정렬한다.
"Provide만 잔뜩 써놓고 왜 아무것도 안 도나요?"가 fx 첫 입문자의 단골 질문. 다음 슬라이드로 바로 이어진다.
기초fx.Invoke

Invoke가 없으면 아무 일도 일어나지 않는다

fx.Provide — lazy

그래프에 올려만 둔다. 부수 효과 없음.

대부분의 NewXxx 생성자

fx.Invoke — eager

앱 시작 시 반드시 실행된다. 여기서 요구한 타입이 그래프 조립의 출발점이 된다.

서버 기동 · 라우터 등록 · 훅 등록

기억법 부수 효과면 Invoke. 값을 만들어 돌려주면 Provide.
// Provide만 있고 Invoke가 없는 앱
→ 생성자는 단 하나도 호출되지 않는다. 앱은 조용히 뜨고 조용히 끝난다.
여기서 손을 들게 해도 좋다 — "Provide 20개 등록하고 Invoke 없이 실행하면 몇 개가 호출될까요?" 답은 0개.
기초fx.Supply

만들 게 없는 값은 Supply

type Config struct {
    DBHost string
    DBPort int
}

cfg := &Config{DBHost: "localhost", DBPort: 3306}

app := fxtest.New(t,
    fx.Supply(cfg),   // 생성자 없이 값 그대로 등록
    fx.Invoke(func(c *Config) {
        // c.DBHost == "localhost"
    }),
)
언제 쓰나 설정 구조체, 상수처럼 생성 로직이 없는 값. Provide로 감싸면 func() *Config { return cfg } 같은 군더더기가 생긴다.
차이 한 줄 Provide생성자 함수를, Supply이미 만든 값을 등록한다.
Supply는 테스트에서 특히 자주 쓴다. 실제 설정 파일을 읽는 대신 구조체를 손으로 만들어 넣는 식.
실전before → after

수동 조립을 fx로 옮기면

// cmd/main.go
app := fx.New(
    fx.Provide(
        config.New,              // *config.Config
        database.New,            // *sql.DB          (*config.Config 필요)
        NewEcho,                 // *echo.Echo

        // 익명 함수도 그대로 생성자가 된다 — 타입만 맞으면 된다
        func(cfg *config.Config) time.Duration {
            return time.Duration(cfg.Context.Timeout) * time.Second
        },

        article.NewArticleHandler,         // Echo, UseCase 필요
        article.NewArticleUsecase,         // Repo, AuthorRepo, Duration 필요
        article.NewMysqlArticleRepository, // DB 필요
        author.NewMysqlAuthorRepository,   // DB 필요
    ),
    fx.Invoke(registerHooks),   // 서버 시작
)
달라진 것 조립 순서가 사라졌다. 남은 건 "무엇이 있는가"의 목록뿐이고, "어떤 순서로"는 fx가 타입에서 읽어낸다.
2번 슬라이드의 수동 코드와 나란히 놓고 보면 효과가 크다. 필요하면 2번으로 잠깐 돌아갔다 와도 좋다.
원리graph

그래프는 시그니처에서 나온다

config.New() → *Config
NewEcho() → *echo.Echo
database.New(*Config) → *sql.DB
ArticleRepo(*sql.DB)
AuthorRepo(*sql.DB)
ArticleUsecase(repo, authorRepo, timeout)
ArticleHandler(echo, usecase)
registerHookslifecycle, echo, cfg

화살표는 의존 방향이다. fx는 이 그래프를 선언 없이 매개변수·반환 타입만으로 만든다. 순환이 생기면 앱 시작 시점에 어디가 물렸는지 알려준다.

"이 그림을 우리가 그린 적이 없다"는 게 포인트. 코드 어디에도 이 순서를 적어두지 않았는데 fx가 만들어낸다.
수명주기fx.Lifecycle

시작과 종료를 짝으로 묶는다

func registerHooks(lifecycle fx.Lifecycle, e *echo.Echo, cfg *config.Config) {
    lifecycle.Append(fx.Hook{
        OnStart: func(context.Context) error {
            fmt.Println("Starting server")
            go e.Start(cfg.Server.Address)
            return nil
        },
        OnStop: func(context.Context) error {
            fmt.Println("Stopping server")
            return nil
        },
    })
}
쓰는 곳 서버 기동/종료, DB 커넥션 open/close처럼 시작과 정리가 짝을 이루는 리소스. fx.Invoke(registerHooks)로 등록한다.
Graceful Shutdown이 공짜로 따라온다는 점을 강조. OnStop은 등록의 역순으로 호출된다.
주의타이밍

서버는 정확히 언제 뜨는가

1
fx.New() / fxtest.New()
fx.Invoke 함수 본문이 즉시 실행된다. 여기서 하는 일은 Lifecycle에 훅을 등록하는 것뿐. (fx.Populate도 이 시점에 채워진다)
2
app.Start(ctx)
등록된 OnStart 훅이 실행된다 — 실제 서버 기동은 여기서 일어난다
3
app.Stop(ctx)
등록된 OnStop 훅이 실행된다 — Graceful Shutdown
Lifecycle이 2단계인 이유 Invoke로 훅을 일찍 등록해두고, 훅 본문 실행Start까지 미루기 위해서다.
여기가 실무에서 제일 많이 헷갈리는 지점. "Invoke에 넣었는데 왜 벌써 실행되죠?" / "왜 아직 안 뜨죠?" 둘 다 이 표로 설명된다.
02

확장 패턴

앱이 커지면 Provide 목록이 수십 줄이 된다. 묶고(Module), 덧씌우고(Decorate), 구분하고(name/group), 감추는(Private) 도구들.

여기부터는 "당장 필요하진 않지만 언젠가 반드시 만나는" 것들. 지금 다 외우지 말고 이런 게 있다는 것만 기억하라고 안내한다.
확장fx.Module · v1.17+

도메인별로 묶는다

var UserModule = fx.Module("user",
    fx.Provide(
        NewMysqlUserRepo,
        NewUserService,
    ),
)

var OrderModule = fx.Module("order",
    fx.Provide(
        NewMysqlOrderRepo,
        NewOrderService,
    ),
)
// 실전: 각 도메인 패키지가 Module 을 노출하고
// main 에서 조합한다
app := fx.New(
    fx.Provide(config.New, database.New, NewEcho),

    article.Module,   // article 도메인
    author.Module,    // author 도메인
    payment.Module,   // payment 도메인

    fx.Invoke(registerHooks),
)
주의 Module이름을 붙여 묶어줄 뿐이다. 안의 Provide는 여전히 전역 그래프에 노출된다. 감추려면 22번의 fx.Private이 필요하다.
"Module = 캡슐화"라고 오해하기 쉬운데 아니다. 이 오해가 22번 슬라이드의 출발점이므로 여기서 미리 심어둔다.
확장fx.Decorate · v1.18+

원본을 안 건드리고 덧씌운다

// 로깅 데코레이터: 기존 UserRepository 를 래핑
type loggingUserRepo struct {
    inner  UserRepository
    logger Logger
}

func (r *loggingUserRepo) FindByID(id int) string {
    r.calls = append(r.calls, fmt.Sprintf("FindByID(%d)", id))
    return r.inner.FindByID(id)   // 원본 호출
}

app := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),
    fx.Decorate(func(repo UserRepository, logger Logger) UserRepository {
        return &loggingUserRepo{inner: repo, logger: logger}
    }),
)
핵심 원본을 매개변수로 받아 같은 타입을 반환하면 fx가 그래프의 그 노드를 교체한다. UserService코드 한 줄 안 고치고 래퍼를 주입받는다.
로깅·캐싱·메트릭 같은 횡단 관심사에 쓴다. Java의 AOP를 떠올리면 이해가 빠른 청중도 있다.
확장fx.Annotate + name:

같은 타입이 둘일 때 — read / write DB

① 등록: 생성자에 이름을 붙인다
fx.Provide(
    fx.Annotate(NewReadDB,
        fx.ResultTags(`name:"readDB"`)),
    fx.Annotate(NewWriteDB,
        fx.ResultTags(`name:"writeDB"`)),
    NewDBService,
)
② 수신: fx.In 구조체로 매칭
type DBParams struct {
    fx.In
    ReadDB  *DBConnection `name:"readDB"`
    WriteDB *DBConnection `name:"writeDB"`
}

func NewDBService(p DBParams) *DBService {
    return &DBService{
        readDB:  p.ReadDB,
        writeDB: p.WriteDB,
    }
}
fx.In 이란 여러 의존성을 하나의 파라미터 구조체로 묶어 받게 해주는 임베디드 마커. 필드마다 태그로 특정 인스턴스를 지목할 수 있다. (반환값을 묶을 땐 fx.Out)
타입이 같으면 fx가 구분할 방법이 없다는 게 출발점. 이름표를 붙여주는 것뿐이라고 설명하면 직관적이다.
확장group:

같은 인터페이스 구현체를 모아서 받는다

fx.Provide(
    fx.Annotate(func() Notifier { return &EmailNotifier{} },
        fx.ResultTags(`group:"notifiers"`)),
    fx.Annotate(func() Notifier { return &SlackNotifier{} },
        fx.ResultTags(`group:"notifiers"`)),
    fx.Annotate(func() Notifier { return &SMSNotifier{} },
        fx.ResultTags(`group:"notifiers"`)),
    NewNotifierService,
)
type NotifierParams struct {
    fx.In
    Notifiers []Notifier `group:"notifiers"`
}

func NewNotifierService(p NotifierParams) *NotifierService {
    return &NotifierService{notifiers: p.Notifiers}
}
이게 좋은 이유 새 구현체를 추가해도 수신 측 코드는 그대로다. 플러그인·핸들러·외부 클라이언트를 모을 때의 정석.
"Notifier 하나 더 추가하려면 어디를 고쳐야 하나?" → Provide 한 줄만. 수신 측은 안 건드린다는 게 핵심 가치.
비교name vs group

개별로 지목할까, 모아서 받을까

패턴용도수신 측구현체가 늘면
name:"X"동일 타입을 개별 식별단일 필드수신 코드도 고쳐야 한다
group:"Y"동일 타입·인터페이스를 모음슬라이스 필드수신 코드는 그대로
name: 이 맞는 상황 read/write DB처럼 역할이 다른 소수의 인스턴스. 각각을 정확히 지목해야 한다.
group: 이 맞는 상황 Notifier·Handler처럼 같은 취급을 받는 구현체 묶음. 하나씩 이름 붙이면 확장할 때마다 아프다.
이 슬라이드 하나만 기억해도 실무에서 헷갈릴 일이 없다. "개별 지목이면 name, 한꺼번에면 group".
확장fx.Private · v1.20+

Module 내부 의존성을 감춘다

PrivateModule := fx.Module("private",
    fx.Provide(
        newInternalDB,
        fx.Private,    // ← 이 Provide 그룹 전체를
                       //   Module 내부 전용으로
    ),
    fx.Provide(newModuleService),  // 이건 외부 노출
)

// 외부에서 *internalDB 를 요청하면?
leakApp := fx.New(
    PrivateModule,
    fx.Populate(&leaked),
    fx.NopLogger,
)
// leakApp.Err() != nil  → 그래프 구성 단계에서 에러
해결하는 문제 DB 핸들·외부 API 클라이언트 같은 인프라 의존성을 다른 Module이 우연히 공유하는 것을 막는다.
범위 *internalDB는 같은 Module 안의 newModuleService만 주입받을 수 있다.
17번에서 심어둔 "Module은 캡슐화가 아니다"의 답. 실무에서 DB 커넥션 풀이 의도치 않게 공유되는 사고를 막아준다.
03

테스트 전략

fx로 조립한 앱을 테스트에서 어떻게 띄우고, 어떻게 Mock을 끼워 넣고, 조립된 인스턴스를 어떻게 꺼내는가.

DI 프레임워크의 실질적 이점이 드러나는 부분. 조립 지점이 한 곳이라 갈아끼우기가 쉽다.
테스트fxtest.New

테스트 전용 앱 컨테이너

app := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),
    fx.Invoke(func(svc *UserService) { /* ... */ }),
)
defer app.RequireStop()
app.RequireStart()
fx.New 대신 쓰는 이유 실패를 t로 리포트하고, 정리를 도와주며, fx 로그가 테스트 출력에 포함된다.
Require 접두사 RequireStart/RequireStop은 실패하면 테스트를 즉시 중단시킨다.
앞 슬라이드들의 예제가 전부 fxtest였다는 점을 여기서 밝히면 자연스럽게 이어진다.
테스트fx.Replace + fx.As

Mock으로 갈아끼우기

app := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),

    // 실제 UserRepository 를 Mock 으로 교체
    fx.Replace(fx.Annotate(&mockUserRepo{}, fx.As(new(UserRepository)))),

    fx.Invoke(func(svc *UserService) {
        result := svc.repo.FindByID(1)
        // result == "mock-user-1"
    }),
)
fx.As가 왜 필요한가 fx는 타입으로 매칭한다. &mockUserRepo{}의 타입은 *mockUserRepo이지 UserRepository가 아니다. fx.As(new(UserRepository))인터페이스 타입으로 등록해야 기존 Provide를 교체한다.
fx 입문자가 가장 자주 넘어지는 함정. Replace만 쓰고 As를 빼면 "교체가 안 되는데요?"가 된다.
테스트Populate vs Invoke

조립된 인스턴스 꺼내기

// 방식 1: Invoke 클로저로 캡처
var svc1 *UserService
app1 := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),
    fx.Invoke(func(s *UserService) {
        svc1 = s
    }),
)
// 방식 2: Populate 로 직접 추출
var svc2 *UserService
app2 := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),
    fx.Populate(&svc2),
)

// 여러 개도 한 번에
fx.Populate(&svc, &logger)
구분fx.Invokefx.Populate
본질함수를 실행한다변수에 값을 채운다
넘기는 것함수(클로저)포인터
고르는 기준꺼낸 뒤 호출·검증까지 할 때꺼내는 것 자체가 목적일 때
Populate는 내부적으로 Invoke로 구현된 편의 함수다. 본질이 같고 목적만 다르다는 걸 알면 선택이 쉬워진다.
04

정리

무엇을 언제 고를지, 그리고 — 어디까지 fx에 맡기지 않을 것인지.

마지막 장. 선택 가이드 표는 자료로 남겨두고, 마지막 메시지(30번)에 시간을 더 쓴다.
정리선택 가이드

하고 싶은 일 → 쓸 메서드

하고 싶은 일메서드기억법
의존성을 그래프에 등록 (lazy)fx.Provide즉시 실행 아님
앱 시작 시 즉시 실행 (서버 기동)fx.Invoke부수 효과면 Invoke
이미 만든 값·설정을 주입fx.Supply상수·구성값
시작/종료 훅 (Graceful Shutdown)fx.LifecycleInvoke 안에서 Append
도메인별로 의존성 묶기fx.Module커진 Provide 분리
기존 의존성에 로깅·캐싱 덧입히기fx.Decorate원본 수정 없이 래핑
동일 타입을 개별 식별 (read/write)Annotate + name:수신 측 단일 필드
동일 인터페이스를 모아서 주입group:수신 측 슬라이스
Module 내부 의존성 숨기기fx.Private인프라 핸들 격리
테스트에서 Mock 주입fx.Replacefx.As로 인터페이스 매칭
테스트에서 인스턴스 꺼내기fx.Populate검증까지면 Invoke
읽어 내려가지 말고 "자료로 가져가시라"고 안내. 대신 앞에서 다룬 것 중 두어 개만 짚어 복습한다.
마무리trade-off

모든 것을 fx로 관리할 필요는 없다

fx가 빛나는 곳

수명주기 관리가 필요한 컴포넌트

DB 연결 · HTTP 서버 · 외부 클라이언트 — 시작과 정리가 짝을 이루고, 여러 곳에서 공유되는 것들

직접 만드는 게 나은 곳

단순한 값 객체와 유틸리티

생성 비용이 없고 수명주기도 없는 것을 그래프에 올리면 얻는 것 없이 간접성만 늘어난다

치르는 대가 리플렉션 기반이라 컴파일 타임 타입 안전성을 일부 포기한다. 대신 상세한 런타임 에러 메시지와 실전 생산성을 얻는다 — 이 거래가 맞는지는 앱 규모가 답한다.
"DI 프레임워크를 쓰면 다 좋아진다"는 인상으로 끝내지 않는 게 중요. 작은 앱에서는 수동 조립이 더 명확하다는 말을 꼭 덧붙인다.
확인클릭하면 답이 열립니다

스스로 답해보기 ①

Provide는 등록만 하고 실행은 미루는 lazy 등록이기 때문이다. 그래프가 조립되려면 그 타입을 요구하는 쪽이 있어야 하고, 그 시작점이 fx.Invoke다. → 슬라이드 09 · 10
Provide생성자 함수를 등록하고 fx가 반환 타입으로 그래프에 연결한다. Supply이미 만들어진 값을 그대로 등록한다. 설정 구조체·상수처럼 생성 로직이 없는 값에 맞다. → 슬라이드 11
각 생성자의 매개변수 타입과 반환 타입만 본다. 등록 순서는 상관없고, 순환 의존성이 있으면 앱 시작 시점에 에러로 알려준다. → 슬라이드 03 · 13
두 시점으로 나뉜다. Invoke 본문은 fx.New() 때 즉시 실행되지만 거기서 하는 일은 훅 등록뿐이고, OnStart 본문은 app.Start(ctx)에서 실행된다. → 슬라이드 15
fx.Private이다. Module은 이름을 붙여 묶어줄 뿐, 안의 Provide는 전역 그래프에 노출된다. 같은 Provide 그룹에 fx.Private을 넣어야 감춰진다. → 슬라이드 17 · 22
한 문제씩 열기 전에 3초쯤 기다린다. Q4는 오답이 많이 나오는 문제라 특히 천천히.
확인클릭하면 답이 열립니다

스스로 답해보기 ②

fx.Decorate다. 원본을 매개변수로 받아 래핑한 같은 타입을 반환하면 fx가 그래프의 해당 노드를 교체한다. 주입받는 쪽은 코드 변경 없이 래퍼를 받는다. → 슬라이드 18
fx.Annotate + fx.ResultTags로 생성자마다 name:"readDB" 같은 이름을 붙이고, 수신 측은 fx.In을 임베드한 구조체 필드에 같은 태그를 단다. → 슬라이드 19
되긴 하지만 나쁜 방법이다. 구현체가 늘 때마다 수신 코드를 고쳐야 한다. group:"notifiers"로 등록하고 []Notifier 슬라이스로 받으면 수신 코드는 그대로다. → 슬라이드 20 · 21
fx는 타입으로 매칭하는데 &mockUserRepo{}의 타입은 *mockUserRepo이지 인터페이스가 아니기 때문이다. fx.Annotate(..., fx.As(new(UserRepository)))로 감싸야 교체된다. → 슬라이드 25
본질은 같다 — Populate는 내부적으로 Invoke로 구현된 편의 함수다. 차이는 목적이다. 담는 것만이 목적이면 Populate, 꺼낸 뒤 호출·검증까지 하면 Invoke. → 슬라이드 26
Q9는 실제로 많이 겪는 함정이라 "당해보신 분?" 하고 물어보면 반응이 온다.
thank you

오늘 정리

  • fx는 생성자의 매개변수·반환 타입만으로 의존성 그래프를 만든다 — 조립 순서를 코드에서 지운다
  • Provide는 lazy 등록, Invoke는 eager 실행. Invoke가 없으면 아무 일도 일어나지 않는다
  • Lifecycle은 2단계다 — New에서 훅을 등록하고, Start에서 본문을 실행한다
  • 앱이 커지면 Module로 묶고, Decorate로 덧씌우고, name/group으로 구분하고, Private으로 감춘다
  • 테스트는 fxtest + Replace(fx.As 잊지 말 것) + Populate
  • 모든 것을 fx로 관리하지 않는다. 수명주기가 있는 컴포넌트에 집중할 때 가장 빛난다
메서드별 학습 예제 github.com/kenshin579/tutorials-go
golang/third-party/fx
실전 적용 (Clean Architecture) github.com/kenshin579/tutorials-go
project-layout/go-clean-arch-v2
저장소 주소를 띄워둔 채로 Q&A를 받는다. 시간이 남으면 6번 지도 슬라이드로 돌아가 못 다룬 메서드를 짚어줘도 좋다.
발표자 노트
← 글 보기 uber/fx 의존성 주입
01 / 32

단축키

→ · Space · PgDn
다음 슬라이드
← · PgUp
이전 슬라이드
Home · End
처음 · 마지막
O
전체 슬라이드 개요
N
발표자 노트
T
밝은 테마 / 어두운 테마
F
전체 화면
Esc
닫기