golang 의존 주입 도구wire 안내

11674 단어 Golang 의존 주입

wire와 의존 주입


Wire은 Golang 의존 주입 도구로 코드를 자동으로 생성하는 방식으로 컴파일링 기간에 의존 주입을 완성한다. 자바 시스템에서 가장 유명한 Spring 프레임워크는 실행할 때 주입한다. 개인적으로wire와 다른 의존 주입의 가장 큰 차이점이라고 생각한다.
종속 주입(Dependency Injection)은 제어 반전(Inversion of Control)이라고도 하는데, 개인이 제어 반전에 대한 정의는 다음과 같습니다.
현재 대상이 필요로 하는 의존 대상은 외부에서 제공(일반적으로 IoC 용기), 외부는 의존 대상의 구조 등 조작을 책임지고 현재 대상은 호출만 책임지며 의존 대상의 구조에 관심이 없다.즉, 객체에 의존하는 제어권이 IoC 컨테이너에 부여됩니다.
다음은 구성을 통해 데이터베이스 연결을 만드는 것과 같은 반전을 제어하는 예입니다.
//  
type DatabaseConfig struct {
    Dsn string 
}

func NewDB(config *DatabaseConfig)(*sql.DB, error) {
    db,err := sql.Open("mysql", config.Dsn)
    if err != nil {
        return nil, err
    }
    // ...
}

fun NewConfig()(*DatabaseConfig,error) {
    //  
    fp, err := os.Open("config.json")
    if err != nil {
        return nil,err
    }
    defer fp.Close()
    //  Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
        return nil,err
    }
    return &config, nil
}

func InitDatabase() {
    cfg, err:=NewConfig()
    if err!=nil {
        log.Fatal(err)
    }
    db,err:=NewDB(cfg)
    if err!=nil {
        log.Fatail(err)
    }
    // db 
}

데이터베이스 설정이 어떻게 되는지, NewDB 방법은 관심이 없다(예제 코드는 NewConfig이 제공한 JSON 설정 대상을 사용한다). NewDB은 DB 대상을 만들고 돌아오는 것만 책임지고 설정 방식과 결합하지 않기 때문에 설정 센터나 다른 방식으로 설정을 제공하더라도 NewDB 코드는 변경할 필요가 없다. 이것이 바로 반전을 제어하는 마력이다!
반전 제어:
현재 대상에 필요한 의존은 스스로 만든다. 즉, 의존 대상의 통제권은 현재 대상에게 있다.
type DatabaseConfig struct {
    Dsn string 
}

func NewDB()(*sql.DB, error) {
    //  
    fp, err := os.Open("config.json")
    if err != nil {
        return nil,err
    }
    defer fp.Close()
    //  Json
    var config DatabaseConfig
    if err:=json.NewDecoder(fp).Decode(&config);err!=nil {
        return nil,err
    }
    //  
    db,err = sql.Open("mysql", config.Dsn)
    if err != nil {
        return
    }
    // ...
}

제어 정전 모드에서 NewDB 방법은 스스로 설정 대상의 창설 작업을 실현해야 한다. 예시에서 Json 프로필을 읽어야 한다. 이것은 강한 결합 코드이다. 프로필의 형식이 Json이 아니라면 NewDB 방법은 오류를 되돌려준다.
의존 주입도 좋지만 아까 예에서 의존 관계를 수동으로 관리하는 것은 상당히 복잡하고 고통스러운 일이기 때문에 다음 내용에서는 골롱의 의존 주입 도구인 와이어를 중점적으로 소개할 것이다.

손수 쓰다

go get github.com/google/wire/cmd/wire을 통해 wire 명령행 도구를 설치하면 됩니다.
본격적으로 시작하기 전에 와이어의 두 가지 개념을 소개해야 한다. ProviderInjector:
  • Provider: 대상을 만드는 방법, 예를 들어 위의 NewDB(DB 대상 제공)과 NewConfig(Database Config 대상 제공) 방법 등이 있습니다.
  • Injector: 대상의 의존에 따라 순서대로 의존 대상을 구성하고 최종적으로 목적 대상을 구성하는 방법, 예를 들어 위에서 InitDatabase 방법.

  • 현재 우리는 wire을 통해 간단한 프로젝트를 실현하고 있다.프로젝트 구조는 다음과 같습니다.
    |--cmd
        |--main.go
        |--wire.go
    |--config
        |--app.json
    |--internal
        |--config
            |--config.go
        |--db
            |--db.go

    config/app.json
    {
      "database": {
        "dsn": "root:root@tcp(localhost:3306)/test"
      }
    }

    internal/config/config.go
    package config
    
    import (
        "encoding/json"
        "github.com/google/wire"
        "os"
    )
    
    var Provider = wire.NewSet(New) //  New Provider, New , Config 
    
    type Config struct {
        Database database `json:"database"`
    }
    
    type database struct {
        Dsn string `json:"dsn"`
    }
    
    func New() (*Config, error) {
        fp, err := os.Open("config/app.json")
        if err != nil {
            return nil, err
        }
        defer fp.Close()
        var cfg Config
        if err := json.NewDecoder(fp).Decode(&cfg); err != nil {
            return nil, err
        }
        return &cfg, nil
    }
    

    internal/db/db.go
    package db
    
    import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
        "github.com/google/wire"
        "wire-example2/internal/config"
    )
    
    var Provider = wire.NewSet(New) //  
    
    func New(cfg *config.Config) (db *sql.DB, err error) {
        db, err = sql.Open("mysql", cfg.Database.Dsn)
        if err != nil {
            return
        }
        if err = db.Ping(); err != nil {
            return
        }
        return db, nil
    }

    cmd/main.go
    package main
    
    import (
        "database/sql"
        "log"
    )
    
    type App struct { //  
        db *sql.DB
    }
    
    func NewApp(db *sql.DB) *App {
        return &App{db: db}
    }
    
    func main() {
        app, err := InitApp() //  wire injector app 
        if err != nil {
            log.Fatal(err)
        }
        var version string
        row := app.db.QueryRow("SELECT VERSION()")
        if err := row.Scan(&version); err != nil {
            log.Fatal(err)
        }
        log.Println(version)
    }

    cmd/wire.go
    주요 파일, 즉 Injector의 핵심 구현:
    // +build wireinject
    
    package main
    
    import (
        "github.com/google/wire"
        "wire-example2/internal/config"
        "wire-example2/internal/db"
    )
    
    func InitApp() (*App, error) {
        panic(wire.Build(config.Provider, db.Provider, NewApp)) //  wire.Build 
    }

    파일이 작성되고 cmd 디렉토리에 들어가서 wire 명령을 실행하면 다음과 같은 출력이 발생합니다.
    C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
    wire: wire-example2/cmd: wrote C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire_gen.go
    wire_gen.go 파일이 성공적으로 생성되었음을 나타냅니다. 파일의 내용은 다음과 같습니다.
    // Code generated by Wire. DO NOT EDIT.
    
    //go:generate go run github.com/google/wire/cmd/wire
    //+build !wireinject
    
    package main
    
    import (
        "wire-example2/internal/config"
        "wire-example2/internal/db"
    )
    
    // Injectors from wire.go:
    
    func InitApp() (*App, error) {
        configConfig, err := config.New()
        if err != nil {
            return nil, err
        }
        sqlDB, err := db.New(configConfig)
        if err != nil {
            return nil, err
        }
        app := NewApp(sqlDB)
        return app, nil
    }

    App 대상을 생성하는 코드가 자동으로 생성된 것을 볼 수 있다.

    Provider 설명

    NewSet 방법을 통해 본 가방의 창설 대상을 Provider으로 성명하여 다른 대상에게 사용하도록 한다.NewSet은 여러 개의 매개 변수를 수신할 수 있습니다. 예를 들어 저희 db 패키지에서 Mysql과 Redis 연결 대상을 만들 수 있다면 다음과 같이 설명할 수 있습니다.
    var Provider = wire.NewSet(NewDB, NewRedis)
    
    func NewDB(config *Config)(*sql.DB,error) { //  
        
    }
    
    func NewRedis(config *Config)(*redis.Client,error) { //  Redis 
    }

    wire.go 파일 설명

    wire.go 파일은 창설 대상을 만드는 곳에 두어야 한다. 예를 들어 우리 ConfigDB 대상은 최종적으로 App에 서비스를 제공하는 것이기 때문에 wire.go 파일은 App이 있는 가방에 넣어야 한다.
    wire.go 파일 이름은 고정된 것이 아니지만, 모두들 습관적으로 이 파일 이름을 부른다.wire.go의 첫 줄 // +build wireinject은 필수입니다. 의미는 다음과 같습니다.
    "wireinject"라는build tag를 추가해야만 이 파일을 컴파일할 수 있습니다. 저희 gobuild main.go는 보통 안 넣어요.따라서 이 파일은 최종 컴파일에 참여하지 않습니다.wire.Build(config.Provider, db.Provider, NewApp) 객체는 configdb 객체로 전송하여 궁극적으로 필요한 App 객체를 작성합니다.

    wire_gen.go 파일 설명


    이 파일은 wire에서 자동으로 생성되므로 수동으로 편집할 필요가 없습니다!!!//+build !wireinject 레이블과 wire.go 파일의 레이블은 다음과 같습니다.
    컴파일할 때
    "wireinject"의build tag가 추가되지 않았습니다. 이 파일은 컴파일에 참여합니다.
    따라서 임의의 시간에 wire.gowire_gen.go은 한 개만 번역에 참여할 수 있다.

    고급 게임 방법


    cleanup 함수


    의존 자원을 만들 때, 어떤 자원을 만들 수 없으면, 다른 자원을 닫아야 할 경우, cleanup 함수를 사용하여 자원을 닫을 수 있습니다.예를 들어 우리는 db.New 방법에 cleanup 함수를 되돌려 데이터베이스 연결을 닫았는데 관련 코드는 다음과 같이 수정되었다(열거되지 않은 코드는 수정하지 않는다).
    internal/db/db.go
    func New(cfg *config.Config) (db *sql.DB, cleanup func(), err error) { //  
        db, err = sql.Open("mysql", cfg.Database.Dsn)
        if err != nil {
            return
        }
        if err = db.Ping(); err != nil {
            return
        }
        cleanup = func() { // cleanup 
            db.Close()
        }
        return db, cleanup, nil
    }

    cmd/wire.go
    func InitApp() (*App, func(), error) { //  
        panic(wire.Build(config.Provider, db.Provider, NewApp))
    }

    cmd/main.go
    func main() {
        app, cleanup, err := InitApp() //  
        if err != nil {
            log.Fatal(err)
        }
        defer cleanup() //  cleanup 
        var version string
        row := app.db.QueryRow("SELECT VERSION()")
        if err := row.Scan(&version); err != nil {
            log.Fatal(err)
        }
        log.Println(version)
    }

    cmd 디렉토리에서 wire 명령을 다시 실행하고 생성된 wire_gen.go은 다음과 같습니다.
    func InitApp() (*App, func(), error) {
        configConfig, err := config.New()
        if err != nil {
            return nil, nil, err
        }
        sqlDB, cleanup, err := db.New(configConfig)
        if err != nil {
            return nil, nil, err
        }
        app := NewApp(sqlDB)
        return app, func() { //  
            cleanup()
        }, nil
    }

    인터페이스 바인딩


    인터페이스 프로그래밍에서 코드가 의존하는 것은 흔히 인터페이스이지 구체적인 struct가 아니다. 이때 관련 코드를 주입하는 것에 의존하면 약간의 수정이 필요하다. 아까의 예를 계속하면 예시적인 수정은 다음과 같다.
    신규 internal/db/dao.go
    package db
    
    import "database/sql"
    
    type Dao interface { //  
        Version() (string, error)
    }
    
    type dao struct { //  
        db *sql.DB
    }
    
    func (d dao) Version() (string, error) {
        var version string
        row := d.db.QueryRow("SELECT VERSION()")
        if err := row.Scan(&version); err != nil {
            return "", err
        }
        return version, nil
    }
    
    func NewDao(db *sql.DB) *dao { //  dao 
        return &dao{db: db}
    }

    internal/db/db.go도 Provider를 수정하여 NewDao 선언을 추가해야 합니다.
    var Provider = wire.NewSet(New, NewDao)

    cmd/main.go 파일 수정:
    package main
    
    import (
        "log"
        "wire-example2/internal/db"
    )
    
    type App struct {
        dao db.Dao //  Dao 
    }
    
    func NewApp(dao db.Dao) *App { //  Dao 
        return &App{dao: dao}
    }
    
    func main() {
        app, cleanup, err := InitApp()
        if err != nil {
            log.Fatal(err)
        }
        defer cleanup()
        version, err := app.dao.Version() //  Dao 
        if err != nil {
            log.Fatal(err)
        }
        log.Println(version)
    }

    cmd 디렉토리에 들어가서 wire 명령을 실행하면 오류 메시지가 표시됩니다.
    C:\Users\Administrator\GolandProjects\wire-example2\cmd>wire
    wire: C:\Users\Administrator\GolandProjects\wire-example2\cmd\wire.go:11:1: inject InitApp: no provider found for wire-example2/internal/db.Dao
            needed by *wire-example2/cmd.App in provider "NewApp" (C:\Users\Administrator\GolandProjects\wire-example2\cmd\main.go:12:6)
    wire: wire-example2/cmd: generate failed
    wire: at least one generate failure
    wire 힌트 inject InitApp: no provider found for wire-example2/internal/db.Dao, 즉 db.Dao 대상을 제공할 수 있는 Provider을 찾지 못했습니다. 우리는 기본적인 db.dao을 제공했고 실현도 Provider을 등록하지 않았습니까?이것도 고의 OOP 디자인이 특이한 점이다.internal/db/db.goProvider 성명을 수정하고 db.*daodb.Dao의 인터페이스 귀속 관계를 추가합니다.
    var Provider = wire.NewSet(New, NewDao, wire.Bind(new(Dao), new(*dao)))
    wire.Bind() 방법의 첫 번째 파라미터는 interface{}이고 두 번째 파라미터는 이다.
    이때 wire 명령을 실행하면 성공할 수 있습니다!

    엔딩

    wire 도구는 아직도 많은 게임 방법이 있지만 필자의 개인적인 업무 경험을 보면 본고에서 소개한 지식을 습득하면 대부분의 장면을 감당할 수 있다!

    좋은 웹페이지 즐겨찾기