LoginSignup
8
10

More than 3 years have passed since last update.

中継サービスにおけるGo言語でのクリーンアーキテクチャの実装例

Last updated at Posted at 2020-06-28

はじめに

レガシーシステムとのAPI連携を前提とした中継サービス開発におけるクリーンアーキテクチャの実装例を紹介します。
中継サービスはDocker・Kubernetes上での運用を想定して、軽量かつメモリ使用量の少ないGo言語で実装します。

記事の目的

本稿のゴールは以下のとおりです。

  • Go言語によるクリーンアーキテクチャの実装例を理解できる
  • 具体的な実装例からクリーンアーキテクチャのメリット・デメリットを理解し、現場における適用要否を判断できる
  • 類似中継サービス開発における参考記事として参照できる

クリーンアーキテクチャとは

ドメイン駆動設計1をベースとして、2012年にRobert C. Martin氏が同氏のブログで提唱したアーキテクチャです。
日本語書籍として、Clean Architecture 達人に学ぶソフトウェアの構造と設計などがあります。

レイヤーと依存関係

クリーンアーキテクチャでは、ソースコードを以下の4つのレイヤーに分離します。

レイヤー 説明
Entities 最重要のビジネスルールをカプセル化したもの。
ユーザインタフェースやセキュリティ等の外部要件に変更が生じても基本的に影響を受けない。
Use Cases アプリケーション特有のビジネスルール。
個社要件や付加ルール(最重要でないビジネスルール)が含まれる。
Interface Adapters クライアントやDB等の外部から受け取ったデータをUse CasesおよびEntitiesが処理しやすい内部用の形式に変換する。または、その逆。
Infrastructures 外部サービスとの接続制御や外部アプリケーションへの入出力制御。
フレームワークやDBなど、詳細な技術についてを置く。

各レイヤーは依存ルールをもたせます。
以下の図のとおり、円の内側のレイヤーにのみ依存することができ、より外側のレイヤーについては依存することができません。
つまり、外側のレイヤーで定義された関数、変数などは内側のレイヤでは扱うことができません。

CleanArchitecture.jpg

メリット

クリーンアーキテクチャを適用することにより、変更に強く、独立したテストが容易に実行できるようになります。

  • フレームワーク非依存:アーキテクチャは、機能満載のソフトウェアのライブラリに依存していない。これにより、システムをフレームワークの成約で縛るのではなく、フレームワークをツールとして利用できる。
  • テスト可能:ビジネスルールは、UI、データベース、ウェブサーバー、その他の外部要素がなくてもテストできる。
  • UI非依存:UIは、システムの他の部分を変更することなく、簡単に変更できる。たとえば、ビジネスルールを変更することなく、ウェブUIをコンソールUIに置き換えることができる。
  • データベース非依存:OracleやSQL ServerをMongo、BigTable、CouchDBなどに置き換えることができる。ビジネスルールはデータベースに束縛されていない。
  • 外部エージェント非依存:ビジネスルールは、外界のインターフェースについて何も知らない。 -- Clean Architecture 達人に学ぶソフトウェアの構造と設計

構成図

実装例を紹介します。
以下のような構成で、中継サービスは、データベース(MySQL)とレガシーシステムへのAPI連携サービスとの接続を行います。
クライアント側へはREST API(HTTP/1.1)を提供し、API連携サービスへは高速なgRPC(HTTP/2)による接続とします。
domain_service.png

シーケンス

本稿では、簡易な例として、ユーザーリソースを一覧で提供するユーザ情報リスト照会APIの実装例を紹介します。
ここで、ユーザーの権限情報はデータベース上に、ユーザーの詳細情報はレガシーシステム側に保持されている前提とします。
中継サービスは、クライアント(WEB側)に次のREST APIエンドポイントを公開し、以下の処理を実装します。

GET /users ユーザ情報リスト照会

  • リクエスト元ユーザが、ユーザリソースの一覧の取得権限があるかどうか、データベースを検索する。
    • リクエスト元ユーザは、リクエストヘッダに付与されるx-useridで判別する。
  • 権限がある場合、レガシーシステムにユーザ情報の一覧を問い合わせ、結果をレスポンスする。
    • レガシーシステムへの問い合わせにはAPI連携サービスのrpc ListUsersを利用する。

list_users.png

ディレクトリ構成

ディレクトリ構成は以下のとおりです。
各レイヤーのソースコードはディレクトリ別に分けられます。

└── relay_service
    ├── common
    │   └── logger
    │       └── app_logger.go
    ├── config
    │   ├── app_logger.go
    │   ├── db_config.go
    │   ├── api_server_config.go
    │   └── grpc_client_config.go
    ├── constant
    │   └── system_constant.go
    ├── domain
    │   └── model
    │       └── users.go
    ├── infrastructure
    │   ├── gorm
    │   │   └── gorm_handler.go
    │   ├── grpc
    │   │   └── client
    │   │       ├── legacy_api_client_handler.go
    │   │       ├── legacy_api_users.go
    │   │       └── dest
    │   │           └── legacy_api
    │   │               ├── users_model.pb.go
    │   │               └── legacy_api.pb.go
    │   └── openapi
    │       └── server
    │           ├── api.go
    │           ├── router.go
    │           └── server.go
    ├── interface
    │   ├── database
    │   │   ├── user_database.go
    │   │   └── sql_handler.go
    │   ├── controller
    │   │   └── user_controller.go
    │   └── gateway
    │       ├── legacy_api_gateway.go
    │       └── legacy_api_handler.go
    ├── usecase
    │   ├── repository
    │   │   ├── user_database_repository.go
    │   │   └── user_client_repository.go
    │   └── service
    │       └── user_service.go
    └── main.go

レイヤーとディレクトリの対応は以下のとおりです。

レイヤー ディレクトリ 説明
Entities domain ユーザ情報のモデルを配置。
本例ではビジネスロジックは無し。
Use Cases usecase repositoryとserviceに分ける。
repositoryはinterface層を呼ぶ出すためのインターフェースを実装し、serviceにビジネスロジックを実装する。
Interface Adapters interface database、gateway、controllerに分ける。
databaseとgatewayはinfrastructure層(gorm、grpc)を呼ぶ出すためのインターフェースを実装し、controllerにREST APIのコントローラーを実装する。
Infrastructures infrastructure 利用するフレームワークや外部アプリケーション毎にgorm、grpc、openapiに分ける。
それぞれの技術詳細を実装。
(その他) common、config、constant 共通的に利用する機能を配置。
commonはロガー等のユーティリティ。config、constantはシステム設定値など。
本稿の趣旨と外れるため、具体的な実装は省略する。

データモデルの定義

ユーザ情報をドメインモデルとしてdomain層(domain -> model)に定義します。
本稿では実装はありませんが、ドメインモデルに閉じた内容であれば、ビジネスロジックもdomain層に実装可能です。

users.go
package model

// ユーザ情報のエンティティ.
type User struct {
    UserId             string
    UserName           string
    UserAuthority      string
    UserType           string
    UserInfo           string
    StartDate          string
    EndDate            string
}

// ユーザ情報のエンティティリスト.
type Users struct {
    UsersArray []User
}

REST APIサーバーの実装

GET /users ユーザ情報リスト照会APIのエンドポイントを公開するための機能の実装を行います。

infrastructure層

infrastructure -> openapi -> serverディレクトリ内の実装を行います。
server.goでサーバ定義を実装し、routers.goにルーティングを実装します。

server.go
package openapi

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "strconv"
    "syscall"
    "time"

    aplogger "github.com/relay-service/common/logger"
    "github.com/relay-service/common/util"
    api_config "github.com/relay-service/config"
    "github.com/relay-service/errorcodes"
    grpc "github.com/relay-service/infrastructure/grpc/client"
    "github.com/relay-service/interface/controller"
    "github.com/relay-service/interface/gateway"
    usecase "github.com/relay-service/usecase/service"
)

var userController*controller.userController

// Run サーバを起動する.
func Run() error {
    logger := aplogger.CreateLogger()

    if err := initLegacyApi(); err != nil {
        return err
    }

    router := NewRouter()

    // Serverの定義
    srv := &http.Server{
        Addr:              ":" + api_config.ServerPort,
        Handler:           router,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            if err != http.ErrServerClosed {
                logger.WriteAppLog(aplogger.Fatal,
                    fmt.Sprintf("Boot Error.[%s]", err.Error()),
                    nil, errorcodes.xxxx)
            }
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT, syscall.SIGKILL)

    <-quit
    logger.WriteAppLog(aplogger.Info, "Stopping server...", nil, nil)

    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(writeTimeoutInt)*time.Millisecond)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        logger.WriteAppLog(aplogger.Error,
            fmt.Sprintf("Boot Error.[%s]", err.Error()),
            nil, errorcodes.xxxx)
        return errorcodes.InternalCodeToMyError(errorcodes.xxxx)
    }
    logger.WriteAppLog(aplogger.Info, "Server stopped. bye!", nil, nil)

    return nil
}

func initLegacyApi() error {
    logger := aplogger.CreateLogger()

    userController = new(controller.UserController)
    userService := new(usecase.UserService)
    userController.Service = UserService
    clientHandler, _ := grpc.InitLegacyApiClient()

    userService.LegacyApi = &gateway.LegacyApiGateway{LegacyApiHandler: clientHandler}
    return nil
}

routers.goOpenAPI Specification v3に準拠したAPI仕様書から、openapi-generatorで自動生成します。
本稿では、以下のようなginフレームワークを利用したソースコードが生成されます。

routers.go
package openapi

import (
    "net/http"

    middleware "github.com/relay-service/infrastructure/openapi/server/middleware"
    "github.com/gin-gonic/gin"
)

// Route is the information for every URI.
type Route struct {
    // Name is the name of this Route.
    Name string
    // Method is the string for the HTTP method. ex) GET, POST etc..
    Method string
    // Pattern is the pattern of the URI.
    Pattern string
    // HandlerFunc is the handler function of this route.
    HandlerFunc gin.HandlerFunc
}

// Routes is the list of the generated Route.
type Routes []Route

// NewRouter returns a new router.
func NewRouter() *gin.Engine {
    gin.SetMode(gin.ReleaseMode)
    router := gin.New()
    router.Use(middleware.CustomLogger(), middleware.CustomRecovery())
    for _, route := range routes {
        switch route.Method {
        case http.MethodGet:
            router.GET(route.Pattern, route.HandlerFunc)
        case http.MethodPost:
            router.POST(route.Pattern, route.HandlerFunc)
        case http.MethodPut:
            router.PUT(route.Pattern, route.HandlerFunc)
        case http.MethodDelete:
            router.DELETE(route.Pattern, route.HandlerFunc)
        }
    }

    return router
}

// Index is the index handler.
func Index(c *gin.Context) {
    c.String(http.StatusOK, "Hello World!")
}

var routes = Routes{
    {
        "Index",
        http.MethodGet,
        "/v1/health",
        Index,
    },

    {
        "ListUsersGet",
        http.MethodGet,
        "/users",
        ListUsersGet,
    },
}

api.goではバリデーションを行い、interface層(controller)を呼び出します。
本稿ではバリデーション処理の詳細(server/validation)は割愛します。

api.go
package openapi

import (
    "context"
    "fmt"
    "net/http"
    "strconv"

    aplogger "github.com/relay-service/common/logger"
    api_config "github.com/relay-service/config"
    "github.com/relay-service/constant"
    "github.com/relay-service/domain/model"
    "github.com/relay-service/errorcodes"
    converter "github.com/relay-service/infrastructure/openapi/server/converter"
    vm "github.com/relay-service/infrastructure/openapi/server/validation/model"
    vu "github.com/relay-service/infrastructure/openapi/server/validation/util"
    "github.com/gin-gonic/gin"
)

// ListUsersGet - ユーザ情報リスト照会
func ListUsersGet (c *gin.Context) {


    // 処理名の設定
    ctx = context.WithValue(ctx, constant.ProcessNameContextKey, constant.ListUsersGet)

    resStatus, resRes := func() (int, interface{}) {

        // リクエストのValidation
        var requestModel vm.ListUserRequestModel

        if status, errModel := vu.RequestFormatValidation(ctx, c, &requestModel); status != http.StatusOK {
            return status, errModel
        }

        request := model.User{UserType: requestModel.UserType}

        res, err := userController.ListUserHandler(ctx, request)

        if err != nil {
            status, response := converter.ErrorToResponse(ctx, err)
            return status, response
        }
        response := converter.ModelToUserResponse(ctx, res)
        return http.StatusOK, response
    }()

    // 処理結果の振り分け
    select {
    case <-ctx.Done():
        // Timeoutのケースのハンドリング
        logger := aplogger.CreateLogger()
        logger.WriteAppLog(aplogger.Error,
            fmt.Sprintf("Time out error. [%s]. [%s]",
                ctx.Err(), constant.GetProcessName(ctx)),
            ctx.Value(constant.TransactionIDContextKey), errorcodes.xxxx)

        status, response := converter.ErrorToResponse(ctx,
            errorcodes.InternalCodeToMyError(errorcodes.xxxx))
        c.JSON(status, response)
        return
    default:
        c.JSON(resStatus, resRes)
        return
    }
}

interface層

interface -> controllerディレクトリ内の実装を行います。
実装するソースコードはuser_controller.goのみです。また、user_controller.goはusecase層(service)を呼び出すだけです。
本来であれば、infrastructure層のデータをusecase層が扱うデータモデルへ変換する処理を実装しますが、本稿ではinfrastructure層で利用したopenapi-generatorによる自動生成コードがデータモデル変換まで実行してくれるため、特段の変換処理は不要です。

user_controller.go
package controller

import (
    "context"

    "github.com/relay-service/domain/model"
    usecase "github.com/relay-service/usecase/service"
)

// UserController コントローラを構成するサービス.
type UserController struct {
    Service *usecase.UserService
}

// ListUserHandler ユーザ情報リスト照会.
func (controller *UserController) ListUserHandler (ctx context.Context, request model.User) (*model.User, error) {
    response, err := controller.Service.ListUserService(ctx, request)

    if err != nil {
        return response, err
    }

    return response,  nil

}

以上で、REST APIサーバとしての機能の実装は完了です。

DBアクセスの実装

データベースからユーザの権限情報を取得するための機能の実装を行います。
データベースの操作はGo言語のORMであるgormを使用します。

infrastructure層

infrastructure -> gormディレクトリ内の実装を行います。
gormによるDB接続処理を実装します。

gorm_handler.go
package gorm

import (
    "fmt"
    "net/url"
    "strconv"
    "time"

    "github.com/relay-service/errorcodes"
    "github.com/relay-service/interface/database"

    aplogger "github.com/relay-service/common/logger"
    "github.com/relay-service/common/util"
    dbconfig "github.com/relay-service/config"

    // mysqlのdriver
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

// DBConnection gormDBの接続用インタフェース.
type DBConnection interface {
    Open(url string) (*gorm.DB, error)
}

// MySQLDBConnection gormDBの接続箇所の実装(mysql)
type MySQLDBConnection struct {
}

// Open 接続箇所の実装
func (db *MySQLDBConnection) Open(connectURL string) (*gorm.DB, error) {
    return gorm.Open(dbconfig.DBDriver, connectURL)
}

type gormHandler struct {
    Conn *gorm.DB
}

// NewDatabase は、DB接続時の初期処理.
func NewDatabase(conn DBConnection) (database.SQLHandler, error) {
    logger := aplogger.CreateLogger()

    // 接続情報の生成.
    _, dbHost := util.GetEnv(dbconfig.DBHostEnvKey)
    _, dbPort := util.GetEnv(dbconfig.DBPortEnvKey)
    _, dbName := util.GetEnv(dbconfig.DBNameEnvKey)
    _, dbUser := util.GetEnv(dbconfig.DBUserEnvKey)
    _, dbPassword := util.GetEnv(dbconfig.DBPasswordEnvKey)

    dbTz := util.GetEnvOrDefault(dbconfig.DBTimeZoneEnvKey, dbconfig.DefaultDBTimeZone)

    dbOption := "charset=utf8mb4&parseTime=True&loc=" + url.QueryEscape(dbTz)
    dbAccess := dbUser
    dbConnect := "tcp(" + dbHost + ":" + dbPort + ")"

    if len(dbPassword) != 0 {
        dbAccess = dbAccess + ":" + dbPassword
    }
    connectURL := dbAccess + "@" + dbConnect + "/" + dbName + "?" + dbOption

    // サーバ起動時のDB接続のリトライ回数
    connectRetryMaxCount := dbconfig.DBConnectRetryMaxCount
    _, tmpRetryMaxCountStr := util.GetEnv(dbconfig.DBRetryMaxCountEnvKey)

    // サーバ起動時のDB接続のインターバル(秒)
    connectRetryWaitTime := dbconfig.DBConnectRetryWaitTime
    _, tmpRetryWaitTimeStr := util.GetEnv(dbconfig.DBRetryIntervalTimeEnvKey)

    var db *gorm.DB
    var err error

    logConnURL := dbUser + "@" + dbConnect + "/" + dbName + "?" + dbOption
    for i := 1; i <= connectRetryMaxCount; i++ {
        db, err = conn.Open(connectURL)
        if err == nil {
            break
        }
        time.Sleep(time.Duration(connectRetryWaitTime) * time.Second)
    }
    if err != nil {
        return nil, errorcodes.InternalErrorCodeToMyError(errorcodes.xxxx)
    }

    // DB接続の内容を設定する。
    idle := dbconfig.DefaultDBIdleConnection
    _, tmpIdleStr := util.GetEnv(dbconfig.DBIdleConnectionEnvKey)
    max := dbconfig.DefaultDBMaxConnection
    _, tmpMaxStr := util.GetEnv(dbconfig.DBMaxConnectionEnvKey)

    lifeTime := dbconfig.DefaultDBConnMaxLifeTime
    _, tmpLifeTimeStr := util.GetEnv(dbconfig.DBConnMaxLifeTimeEnvKey)

    db.DB().SetMaxIdleConns(idle)
    db.DB().SetMaxOpenConns(max)
    db.DB().SetConnMaxLifetime(time.Duration(lifeTime) * time.Second)

    gormHandler := new(gormHandler)
    gormHandler.Conn = db

    return gormHandler, nil
}

func (handler *gormHandler) Query(value interface{}, find interface{}, query interface{}, args ...interface{}) error {
    return handler.Conn.Model(value).Where(query, args...).Find(find).Error
}

func (handler *gormHandler) Close() error {
    return handler.Conn.Close()
}

func (handler *gormHandler) Where(value interface{}, args ...interface{}) database.SQLHandler {
    newHandler := new(gormHandler)
    newHandler.Conn = handler.Conn.Where(value, args...)
    return newHandler
}

これでデータベース接続ができるようになりました。
次に、データベースから取得した情報を、interface層でドメインモデルに変換する必要があります。
しかし、ここで問題が発生します。

データベースは外部アプリケーションであるため、クリーンアーキテクチャのレイヤーの考え方に則り、infrastructure層に実装しました。
しかし、依存関係のルールのとおり、円の外側のレイヤーを内側のレイヤーから呼び出すことはできません。
つまり、infrastructure層でデータベースから取得した情報をinterface層で利用することができないのです。

この矛盾は依存関係逆転の原則(DIP)によって解決されます。

依存関係逆転の原則(DIP)

A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
B. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
-- 依存性逆転の原則

つまり、抽象利用することで、内側のレイヤー(interface層)から外側のレイヤー(infrastructure層)を利用することができるようになります。
Go言語ではこれをinterfaceを定義することで実現します。
インターフェースは内側のレイヤー側に実装します。

DIP.png

interface層

interface -> databaseディレクトリ内の実装を行います。
まず、sql_handler.goに上述のinterfaceを実装し、gormを実行できるようにします。

sql_handler.go
package database

// SQLHandler databaseからinfrastructureを実行するためのinterface.
type SQLHandler interface {
    Query(value interface{}, find interface{}, query interface{}, args ...interface{}) error
    Where(value interface{}, args ...interface{}) SQLHandler
    Close() error
}

sql_handler.goからinfrastructure層のgormを実行できるようになりました。
user_database.goからsql_handler.goに実装したinterfaceを経由してgormを実行します。

user_database.go
package database

import (
    "context"
    "fmt"

    aplogger "github.com/relay-service/common/logger"
    usecase "github.com/relay-service/usecase/service"
    "github.com/relay-service/constant"
    "github.com/relay-service/errorcodes"
)

// UserDatabase Databaseが利用するインタフェースの定義
type UserDatabase struct {
    SQLHandler
}

// Query ユーザ情報の取得.
func (userDatabase *UserDatabase) Query(ctx context.Context, value interface{}, find interface{}, query interface{}, filters []interface{}, args ...interface{}) (interface{}, error) {

    // フィルターに指定されている検索条件を設定する.
    queryHandler := addFilter(filters, payEnterpriseDatabase.SQLHandler)

    if err := queryHandler.Query(value, find, query, args...); err != nil {
        logger := aplogger.CreateLogger()

        // DBエラーを内部エラーコード、内部エラーにMappingする.
        myErrCd, myErr := userDatabase.SQLHandler.DBErrorToInternalError(err)

        // 内部エラーコードを元に、Databaseに関するエラーのエラーハンドリングする.
        switch myErrCd {
        // 内部処理エラー(DB)
        case errorcodes.xxxx:
            logger.WriteAppLog(aplogger.Error,
                fmt.Sprintf("DB access error. [%s]", constant.GetProcessName(ctx)),
                ctx.Value(constant.TransactionIDContextKey), errorcodes.xxxx)
        // SQLTimeOut
        case errorcodes.xxxx:
            logger.WriteAppLog(aplogger.Error,
                fmt.Sprintf("DB access timed out.[%s]", constant.GetProcessName(ctx)),
                ctx.Value(constant.TransactionIDContextKey), errorcodes.ERRCD0E0102)
        }

        return nil, myErr
    }

    return value, nil
}

// Filterの内容を、SQLの検索条件に変換して、条件を追加する.
func addFilter(filters []interface{}, baseHandler SQLHandler) SQLHandler {

    for _, filter := range filters {
        switch filter.(type) {
        // MasterIsEnableFilterの場合の検索フィルターを追加
        case usecase.MasterIsEnableFilter:
            sf, _ := filter.(usecase.MasterIsEnableFilter)
                        // リクエスト元ユーザの権限をチェック(user_authority == '1')する.
            baseHandler = baseHandler.Where("user_id == ? and user_authority == '1'", sf.ReferenceUserId)
        }
    }
    return baseHandler
}

以上で、DBアクセス機能の実装は完了です。

gRPCクライアントの実装

レガシーシステムAPI連携サービスとのgRPC接続機能の実装を行います。

infrastructure層

infrastructure -> grpc -> clientディレクトリ内の実装を行います。
legacy_api_client_handler.golegacy_api.pb.gousers_model.pb.goでgRPCクライアントを定義します。

legacy_Api_client_handler.go
package grpc

import (
    "context"
    "fmt"
    "strconv"
    "time"

    aplogger "github.com/relay-service/common/logger"
    "github.com/relay-service/common/util"
    "github.com/relay-service/constant"
    "github.com/relay-service/errorcodes"

    api_config "github.com/relay-service/config"
    "github.com/relay-service/domain/model"
    pb "github.com/relay-service/infrastructure/grpc/client/dest/legacy_api"
    gw "github.com/relay-service/interface/gateway"
    "google.golang.org/grpc"
)

// legacyApiTimeout legacyApiのタイムアウト時間(ミリ秒)
var legacyApiTimeout time.Duration

// LegacyApiClientHandler Handler
type LegacyApiClientHandler struct {
    Client pb.LegacyApiClient
    conn   *grpc.ClientConn
}

// InitLegacyApiClient Clientの生成.
func InitLegacyApiClient() (gw.LegacyApiHandler, error) {
    conreq := ClientConnectionRequest{
        AddressKey:                  api_config.LegacyApiAddress,
        TimeoutKey:                  api_config.LegacyApiTimeout,
        GrpcKeepAliveTimeKey:        api_config.GrpcKeepAliveTime,
        GrpcKeepAliveTimeoutKey:     api_config.GrpcKeepAliveTimeout,
        DefaultTimeout:              api_config.DefaultLegacyApiTimeout,
        DefaultGrpcKeepAliveTime:    api_config.DefaultGrpcKeepAliveTime,
        DefaultGrpcKeepAliveTimeout: api_config.DefaultGrpcKeepAliveTimeout,
    }

    conn, err := NewClientConnection(conreq)

    if err != nil {
        return nil, err
    }

    // クライアントの接続タイム
    logger := aplogger.CreateLogger()
    _, timeout := util.GetEnv(api_config.LegacyApiTimeout)

    timeoutInt, _ := strconv.ParseInt(timeout, 10, 64)
    legacyApiTimeout = time.Duration(timeoutInt) * time.Millisecond

    legacyApiClientHandler := new(LegacyApiClientHandler)
    legacyApiClientHandler.conn = conn
    legacyApiClientHandler.Client = pb.NewLegacyApiClient(conn)

    return legacyApiClientHandler, nil
}

// ListUsers ユーザ情報リスト照会. 
func (legacyApiClientHandler *LegacyApiClientHandler) ListUsers(ctx context.Context, user model.User) (model.Users, error) {

    message := &pb.ListUsersRequest{
        UserId:           userId
    }

    reqCtx, cancel := context.WithTimeout(ctx, legacyApiTimeout)
    defer cancel()
    listUsersResponse, err := legacyApiClientHandler.Client.ListUsers(reqCtx, message)

    var response model.Users
    if err != nil {
        return response,  err
    }

    var userArray []model.User

    for _, user:= range listUserResponse.User {
        userArray = append(userArray, model.User{
            UserId:             user.UserId,
            UserName:           user.UserName,
            UserType:           user.UserType,
            UserInfo:           user.UserInfo,
            StartDate:          user.StartDate,
            EndDate:            user.EndDate,
        })
    }

    response.UserArray = userArray

    return response, nil
}

// Close クローズ
func (legacyApiClientHandler *LegacyApiClientHandler) Close() error {
    return legacyApiClientHandler.conn.Close()
}

type ClientConnectionRequest struct {
    AddressKey                  string
    TimeoutKey                  string
    GrpcKeepAliveTimeKey        string
    GrpcKeepAliveTimeoutKey     string
    DefaultTimeout              string
    DefaultGrpcKeepAliveTime    string
    DefaultGrpcKeepAliveTimeout string
}

// NewClientConnection Connectionの生成.
func NewClientConnection(req ClientConnectionRequest) (*grpc.ClientConn, error) {
    logger := aplogger.CreateLogger()

    _, address := util.GetEnv(req.AddressKey)
    var err error
    _, timeout := util.GetEnv(req.TimeoutKey)
    _, keepAliveTime := util.GetEnv(req.GrpcKeepAliveTimeKey)
    _, keepAliveTimeout := util.GetEnv(req.GrpcKeepAliveTimeoutKey)

    timeoutInt, _ := strconv.ParseInt(timeout, 10, 64)
    keepAliveTimeInt, _ := strconv.ParseInt(keepAliveTime, 10, 64)
    keepAliveTimeoutInt, _ := strconv.ParseInt(keepAliveTimeout, 10, 64)

    conn, err := grpc.Dial(address,
        grpc.WithInsecure(),
        grpc.WithTimeout(time.Duration(timeoutInt)*time.Millisecond),
        grpc.WithKeepaliveParams(keepalive.ClientParameters{
            Time:                time.Duration(keepAliveTimeInt) * time.Millisecond,
            Timeout:             time.Duration(keepAliveTimeoutInt) * time.Millisecond,
            PermitWithoutStream: true,
        }),
        grpc.WithUnaryInterceptor(ClientLogger()))

    return conn, err
}

// ClientLogger ログ出力
func ClientLogger() grpc.UnaryClientInterceptor {
    return func(
        ctx context.Context,
        method string,
        req interface{},
        reply interface{},
        cc *grpc.ClientConn,
        invoker grpc.UnaryInvoker,
        opts ...grpc.CallOption,
    ) error {
        logger := aplogger.CreateLogger()

        logger.WriteTelegramLog(ctx.Value(constant.TransactionIDContextKey), req, aplogger.RequestTo)

        err := invoker(ctx, method, req, reply, cc, opts...)

        if err != nil {
            logger.WriteTelegramLog(ctx.Value(constant.TransactionIDContextKey), err, aplogger.ResponseFrom)
        } else {
            logger.WriteTelegramLog(ctx.Value(constant.TransactionIDContextKey), reply, aplogger.ResponseFrom)
        }

        return err
    }
}

gRPCクライアント定義はProtocol Buffersで定義します。
具体的な定義であるlegacy_api.pb.gousers_model.pb.goは、protocプラグインで自動生成することができます。
本稿では、以下のようなソースコードが生成されます。

legacy_api.pb.go
package Legacy_api

import (
    context "context"
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    grpc "google.golang.org/grpc"
    codes "google.golang.org/grpc/codes"
    status "google.golang.org/grpc/status"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

//*
// ユーザ情報リスト照会リクエスト
type ListUsersRequest struct {
    UserId               string     `protobuf:"bytes,4,opt,name=user_id,json=UserId,proto3" json:"user_id,omitempty"`
    XXX_NoUnkeyedLiteral struct{}   `json:"-"`
    XXX_unrecognized     []byte     `json:"-"`
    XXX_sizecache        int32      `json:"-"`
}

func (m *ListUsersRequest) Reset()         { *m = ListUsersRequest{} }
func (m *ListUsersRequest) String() string { return proto.CompactTextString(m) }
func (*ListUsersRequest) ProtoMessage()    {}
func (*ListUsersRequest) Descriptor() ([]byte, []int) {
    return fileDescriptor_3bd259c5e14036ee, []int{0}
}

func (m *ListUsersRequest) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_ListUsersRequest.Unmarshal(m, b)
}
func (m *ListUsersRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_ListUsersRequest.Marshal(b, m, deterministic)
}
func (m *ListUsersRequest) XXX_Merge(src proto.Message) {
    xxx_messageInfo_ListUsersRequest.Merge(m, src)
}
func (m *ListUsersRequest) XXX_Size() int {
    return xxx_messageInfo_ListUsersRequest.Size(m)
}
func (m *ListUsersRequest) XXX_DiscardUnknown() {
    xxx_messageInfo_ListUsersRequest.DiscardUnknown(m)
}

var xxx_messageInfo_ListUsersRequest proto.InternalMessageInfo

func (m *ListUsersRequest) GetUserId() string {
    if m != nil {
        return m.UserId
    }
    return ""
}

//*
// 複数のユーザ情報を返す際のレスポンス
type ListUsersResponse struct {
    User                 []*User   `protobuf:"bytes,1,rep,name=User,proto3" json:"User,omitempty"`
    XXX_NoUnkeyedLiteral struct{}  `json:"-"`
    XXX_unrecognized     []byte    `json:"-"`
    XXX_sizecache        int32     `json:"-"`
}

func (m *ListUsersResponse) Reset()         { *m = ListUsersResponse{} }
func (m *ListUsersResponse) String() string { return proto.CompactTextString(m) }
func (*ListUsersResponse) ProtoMessage()    {}
func (*ListUsersResponse) Descriptor() ([]byte, []int) {
    return fileDescriptor_3bd259c5e14036ee, []int{1}
}

func (m *ListUsersResponse) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_ListUsersResponse.Unmarshal(m, b)
}
func (m *ListUsersResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_ListUsersResponse.Marshal(b, m, deterministic)
}
func (m *ListUsersResponse) XXX_Merge(src proto.Message) {
    xxx_messageInfo_ListUsersResponse.Merge(m, src)
}
func (m *ListUsersResponse) XXX_Size() int {
    return xxx_messageInfo_ListUsersResponse.Size(m)
}
func (m *ListUsersResponse) XXX_DiscardUnknown() {
    xxx_messageInfo_ListUsersResponse.DiscardUnknown(m)
}

var xxx_messageInfo_ListUsersResponse proto.InternalMessageInfo

func (m *ListUsersResponse) GetUser() []*User {
    if m != nil {
        return m.User
    }
    return nil
}


func init() {

func init() {
    proto.RegisterFile("legacy_api.proto", fileDescriptor_3bd259c5e14036ee)
}

var fileDescriptor_3bd259c5e14036ee = []byte{
    // 416 bytes of a gzipped FileDescriptorProto
    0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x93, 0xd1, 0x6e, 0xd3, 0x30,
    0x14, 0x86, 0x71, 0xb2, 0x14, 0xe1, 0x69, 0x25, 0xb2, 0x10, 0x0a, 0x41, 0x42, 0x51, 0x25, 0x44,
    0x2e, 0x58, 0x54, 0xca, 0x25, 0x57, 0x23, 0x1d, 0x30, 0x18, 0x6d, 0x14, 0x98, 0x80, 0x2b, 0xcb,
    0x8d, 0x4f, 0x91, 0xa1, 0x8b, 0x8d, 0xed, 0x21, 0xe5, 0x05, 0x78, 0x01, 0x1e, 0x82, 0x17, 0xe1,
    0xb5, 0x90, 0x50, 0x92, 0x4e, 0x6d, 0x43, 0x11, 0x54, 0x70, 0xf9, 0xff, 0x39, 0xe7, 0x3f, 0x27,
    0x9f, 0x6d, 0x1c, 0x29, 0x56, 0x1d, 0x42, 0x69, 0x41, 0x2b, 0x2d, 0x0c, 0x98, 0x43, 0x0d, 0x4a,
    0x1a, 0x61, 0xa5, 0xae, 0x12, 0xa5, 0xa5, 0x95, 0xe4, 0xfe, 0x07, 0x0e, 0x33, 0x61, 0x93, 0x19,
    0x2b, 0x3e, 0x42, 0xc9, 0x13, 0xc5, 0x2a, 0xba, 0xd6, 0x40, 0xd7, 0x1a, 0x3e, 0x3f, 0x08, 0x6f,
    0x77, 0xf3, 0xce, 0x25, 0x87, 0x45, 0x1b, 0x35, 0xf8, 0xe1, 0xe0, 0x5b, 0xa7, 0xc2, 0xd8, 0x8c,
    0x55, 0xc7, 0xab, 0x92, 0x1c, 0x3e, 0x5d, 0x80, 0xb1, 0xe4, 0x0b, 0xc2, 0xa4, 0x60, 0x73, 0x61,
    0xa8, 0xd2, 0x92, 0x5f, 0x14, 0x96, 0x16, 0x92, 0x43, 0x80, 0x22, 0x14, 0xf7, 0x47, 0x6f, 0x93,
    0x5d, 0xd6, 0x48, 0x7e, 0x3b, 0x25, 0x49, 0xeb, 0x09, 0x59, 0x3b, 0x20, 0x95, 0x1c, 0x72, 0xbf,
    0xe8, 0x38, 0xe4, 0x06, 0xf6, 0x16, 0xe2, 0x5c, 0xd8, 0xc0, 0x89, 0x50, 0xec, 0xe5, 0xad, 0x20,
    0x37, 0x71, 0x4f, 0xce, 0xe7, 0x06, 0x6c, 0xe0, 0x36, 0xf6, 0x52, 0x91, 0xbb, 0xb8, 0x6f, 0x35,
    0x2b, 0x0d, 0x2b, 0xac, 0x90, 0x25, 0x15, 0x3c, 0xd8, 0x8b, 0x50, 0x7c, 0x2d, 0x3f, 0x58, 0x73,
    0x4f, 0x38, 0xb9, 0x87, 0xaf, 0x2b, 0x2d, 0x0b, 0x30, 0x46, 0x94, 0xef, 0x29, 0x67, 0x16, 0x02,
    0xaf, 0xa9, 0xeb, 0xaf, 0xec, 0x31, 0xb3, 0x30, 0x78, 0x87, 0xfd, 0xee, 0x8e, 0xe4, 0x0e, 0x0e,
    0xd3, 0xa3, 0x27, 0x27, 0xaf, 0x68, 0x96, 0x4f, 0xc7, 0x67, 0xe9, 0x6b, 0x9a, 0x4e, 0xc7, 0xc7,
    0xf4, 0x6c, 0xf2, 0x62, 0x32, 0x7d, 0x33, 0xf1, 0xaf, 0x90, 0x7d, 0x7c, 0xf5, 0xe5, 0x51, 0x76,
    0x3a, 0x1c, 0x0e, 0x7d, 0x54, 0x8b, 0xc7, 0x4b, 0xe1, 0xd4, 0xe2, 0xf9, 0x52, 0xb8, 0x83, 0xaf,
    0x0e, 0x0e, 0xb7, 0x91, 0x31, 0x4a, 0x96, 0x06, 0x08, 0xc3, 0x07, 0x1b, 0x5f, 0x02, 0x14, 0xb9,
    0xf1, 0xfe, 0xe8, 0xd1, 0x6e, 0xe8, 0x37, 0x22, 0xf2, 0xcd, 0xc4, 0x1a, 0xad, 0x95, 0x96, 0x2d,
    0x2e, 0xd1, 0x36, 0x62, 0x05, 0xdc, 0xdd, 0x0e, 0x7c, 0xef, 0x0f, 0xc0, 0xbd, 0xbf, 0x04, 0xde,
    0xdb, 0x06, 0x7c, 0xf4, 0x1d, 0xe1, 0xa0, 0x4b, 0xe4, 0xf2, 0x87, 0xc8, 0x37, 0x84, 0xc9, 0xaf,
    0xc8, 0xc8, 0xd3, 0xff, 0x74, 0x1d, 0xc3, 0x67, 0xff, 0x1e, 0xd4, 0x9e, 0xde, 0xac, 0xd7, 0xbc,
    0xb1, 0x87, 0x3f, 0x03, 0x00, 0x00, 0xff, 0xff, 0xaf, 0x6b, 0xb3, 0x5c, 0xd2, 0x03, 0x00, 0x00,
}

// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6

// LegacyApiClient is the client API for LegacyApi service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type LegacyApiClient interface {
    //*
    // ユーザ情報リスト取得
    ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)
}

type LegacyApiClient struct {
    cc grpc.ClientConnInterface
}

func NewLegacyApiClient(cc grpc.ClientConnInterface) LegacyApiClient {
    return &legacyApiClient{cc}
}

func (c *legacyApiClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) {
    out := new(ListUsersResponse)
    err := c.cc.Invoke(ctx, "/legacy_api.LegacyApi/ListUsers", in, out, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}

// LegacyApiServer is the server API for LegacyApi service.
type LegacyApiServer interface {
    //*
    // ユーザ情報リスト照会
    ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
}

// UnimplementedLegacyApiServer can be embedded to have forward compatible implementations.
type UnimplementedLegacyApiServer struct {
}

func (*UnimplementedLegacyApiServer) ListUsers(ctx context.Context, req *ListUsersRequest) (*ListUsersResponse, error) {
    return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented")
}

func RegisterLegacyApiServer(s *grpc.Server, srv LegacyApiServer) {
    s.RegisterService(&_LegacyApi_serviceDesc, srv)
}

func _LegacyApi_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    in := new(ListUsersRequest)
    if err := dec(in); err != nil {
        return nil, err
    }
    if interceptor == nil {
        return srv.(LegacyApiServer).ListUsers(ctx, in)
    }
    info := &grpc.UnaryServerInfo{
        Server:     srv,
        FullMethod: "/legacy_api.LegacyApi/ListUsers",
    }
    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
        return srv.(LegacyApiServer).ListUsers(ctx, req.(*ListUsersRequest))
    }
    return interceptor(ctx, in, info, handler)
}

var _LegacyApi_serviceDesc = grpc.ServiceDesc{
    ServiceName: "legacy_api.LegacyApi",
    HandlerType: (*LegacyApiServer)(nil),
    Methods: []grpc.MethodDesc{
        {
            MethodName: "ListUsers",
            Handler:    _LegacyApi_ListUsers_Handler,
        },
    },
    Streams:  []grpc.StreamDesc{},
    Metadata: "legacy_api.proto",
}
users_model.pb.go
package legacy_api

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package

//*
// ユーザ情報
type User struct {
    UserId               string   `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
    UserName             string   `protobuf:"bytes,2,opt,name=user_name,json=userName,proto3" json:"user_name,omitempty"`
    UserType             string   `protobuf:"bytes,4,opt,name=user_type,json=userType,proto3" json:"user_type,omitempty"`
    UserInfo             string   `protobuf:"bytes,5,opt,name=user_info,json=userInfo,proto3" json:"user_info,omitempty"`
    StartDate            string   `protobuf:"bytes,9,opt,name=start_date,json=startDate,proto3" json:"start_date,omitempty"`
    EndDate              string   `protobuf:"bytes,10,opt,name=end_date,json=endDate,proto3" json:"end_date,omitempty"`
    XXX_NoUnkeyedLiteral struct{} `json:"-"`
    XXX_unrecognized     []byte   `json:"-"`
    XXX_sizecache        int32    `json:"-"`
}

func (m *User) Reset()         { *m = User{} }
func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage()    {}
func (*User) Descriptor() ([]byte, []int) {
    return fileDescriptor_f1a48adbc23b586f, []int{0}
}

func (m *User) XXX_Unmarshal(b []byte) error {
    return xxx_messageInfo_User.Unmarshal(m, b)
}
func (m *User) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
    return xxx_messageInfo_User.Marshal(b, m, deterministic)
}
func (m *User) XXX_Merge(src proto.Message) {
    xxx_messageInfo_User.Merge(m, src)
}
func (m *User) XXX_Size() int {
    return xxx_messageInfo_User.Size(m)
}
func (m *User) XXX_DiscardUnknown() {
    xxx_messageInfo_User.DiscardUnknown(m)
}

var xxx_messageInfo_User proto.InternalMessageInfo

func (m *User) GetUserId() string {
    if m != nil {
        return m.UserId
    }
    return ""
}

func (m *User) GetUserName() string {
    if m != nil {
        return m.UserName
    }
    return ""
}

func (m *User) GetUserType() string {
    if m != nil {
        return m.UserType
    }
    return ""
}

func (m *User) GetUserInfo() string {
    if m != nil {
        return m.UserInfo
    }
    return ""
}

func (m *User) GetStartDate() string {
    if m != nil {
        return m.StartDate
    }
    return ""
}

func (m *User) GetEndDate() string {
    if m != nil {
        return m.EndDate
    }
    return ""
}

func init() {
    proto.RegisterType((*User)(nil), "legacy_api.LegacyApi")
}

func init() {
    proto.RegisterFile("legacy_api.proto", fileDescriptor_f1a48adbc23b586f)
}

var fileDescriptor_f1a48adbc23b586f = []byte{
    // 348 bytes of a gzipped FileDescriptorProto
    0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0xd1, 0xdf, 0x6a, 0x22, 0x31,
    0x14, 0xc7, 0x71, 0x5c, 0x77, 0xfd, 0x93, 0xc5, 0x5d, 0x8d, 0x2b, 0x3b, 0xa5, 0x14, 0x8a, 0x57,
    0x52, 0x74, 0xc0, 0x96, 0xbe, 0x40, 0xff, 0x40, 0xbd, 0xb0, 0x48, 0x5b, 0xaf, 0xc3, 0x31, 0x39,
    0x53, 0x53, 0x67, 0x92, 0x90, 0x64, 0x84, 0x79, 0xba, 0xbe, 0x5a, 0x99, 0xcc, 0x20, 0xda, 0xdb,
    0xf3, 0xfd, 0xfc, 0x20, 0x10, 0x72, 0x6e, 0xa0, 0x98, 0xa1, 0xf2, 0x68, 0x8d, 0x95, 0x0e, 0xdd,
    0x2c, 0xd3, 0x02, 0xd3, 0xd8, 0x58, 0xed, 0x35, 0x9d, 0x7e, 0x08, 0xdc, 0x48, 0x1f, 0x6f, 0x80,
    0xef, 0x50, 0x89, 0xd8, 0x40, 0xc1, 0x8e, 0x2c, 0xb3, 0x68, 0xb4, 0x93, 0x5e, 0xdb, 0x22, 0xde,
    0xcf, 0xc7, 0x9f, 0x4d, 0xd2, 0x5b, 0x41, 0xf1, 0x78, 0x00, 0xf4, 0x8a, 0x0c, 0x4e, 0x27, 0x4c,
    0x8a, 0xa8, 0x71, 0xd9, 0x98, 0x74, 0x5f, 0xfe, 0x9a, 0x63, 0xb9, 0x10, 0x34, 0x26, 0xc3, 0x6f,
    0x56, 0x41, 0x86, 0xd1, 0x8f, 0xa0, 0x07, 0x27, 0xfa, 0x19, 0x32, 0xa4, 0x13, 0xd2, 0x2f, 0xbd,
    0x43, 0xbb, 0x97, 0xbc, 0xc6, 0xcd, 0x80, 0xff, 0x18, 0x28, 0x5e, 0xab, 0x73, 0x90, 0x73, 0x32,
    0x32, 0x92, 0x65, 0xa0, 0xe0, 0x1d, 0x33, 0x54, 0x9e, 0xe5, 0x4a, 0xfa, 0xf2, 0x25, 0x3f, 0x03,
    0xa7, 0x46, 0x2e, 0x0f, 0x6d, 0xad, 0xa4, 0x5f, 0x08, 0x3a, 0x25, 0x94, 0x43, 0x22, 0x1d, 0x33,
    0x56, 0x8b, 0x9c, 0x7b, 0xc6, 0xb5, 0xc0, 0xe8, 0x57, 0xf0, 0xfd, 0x50, 0x56, 0x55, 0xb8, 0xd7,
    0x02, 0xe9, 0x35, 0x19, 0x01, 0xe7, 0x3a, 0x57, 0x9e, 0x79, 0x0b, 0xca, 0x25, 0x68, 0xab, 0x41,
    0x2b, 0x0c, 0x86, 0x75, 0x7c, 0xab, 0x5b, 0xd8, 0xdc, 0x92, 0xff, 0xd2, 0xb1, 0xad, 0x4e, 0x05,
    0x0a, 0x96, 0xa1, 0xe5, 0x5b, 0x50, 0x9e, 0x49, 0x95, 0xe8, 0xa8, 0x1d, 0x56, 0xff, 0xa4, 0x7b,
    0x0a, 0x75, 0x59, 0xc7, 0x85, 0x4a, 0x34, 0x1d, 0x93, 0x1e, 0x87, 0x34, 0x65, 0xe5, 0x8f, 0xb0,
    0xdc, 0xa6, 0x51, 0x27, 0xe0, 0xdf, 0xe5, 0xf1, 0x0e, 0xf8, 0x6e, 0x6d, 0x53, 0x7a, 0x41, 0x88,
    0xf3, 0x60, 0x3d, 0x13, 0xe0, 0x31, 0xea, 0x06, 0xd0, 0x0d, 0x97, 0x07, 0xf0, 0x48, 0xcf, 0x48,
    0x07, 0x95, 0xa8, 0x22, 0x09, 0xb1, 0x8d, 0x4a, 0x94, 0x69, 0xd3, 0x0a, 0xdf, 0x7e, 0xf3, 0x15,
    0x00, 0x00, 0xff, 0xff, 0xa8, 0xe3, 0xbb, 0x4e, 0x15, 0x02, 0x00, 0x00,
}

interface層

interface -> gatewayディレクトリ内の実装を行います。
openapi-generatorと同様に、ドメインモデルへのデータモデル変換までinfrastructure層で実装してくれるため、interface層でのデータ変換処理の実装は不要です。
DBアクセス機能と同様に、このままではgRPCクライアント機能がinfrastructure層に閉じてしまっているため、interface層にインターフェースを実装します。

legacy_api_handle.go
package gateway

import (
    "context"

    "github.com/relay-service/domain/model"
)

// LegacyApiHandler interfaceからinfrastructureを実行するためのinterface.
type LegacyApiHandler interface {
    // ユーザ情報リスト照会
    ListUsers(ctx context.Context, request model.User) (model.Users, error)
    Close() error
    ErrorToInternalErrorCode(err error) (string, error)
}

legacy_api_gateway.goからlegacy_api_handle.goに実装したinterfaceを経由してgRPCクライアントを実行します。

legacy_api_gateway.go
package gateway

import (
    "context"
    "fmt"

    aplogger "github.com/relay-service/common/logger"
    "github.com/relay-service/constant"
    "github.com/relay-service/domain/model"
    "github.com/relay-service/errorcodes"
)

// LegacyApiGateway infrastructureのDB関連
type LegacyApiGateway struct {
    LegacyApiHandler
}

// ListUsers ユーザ情報リスト照会.
func (legacyApiGateway *LegacyApiGateway) ListUsers(ctx context.Context, query model.User) (model.Users, error) {

    response, err := legacyApiGateway.LegacyApiHandler.ListUsers(ctx, query)
    if err != nil {
        // gRPCのエラーを内部のエラーコードにMappingする.
        iecd, gatewayErr := legacyApiGateway.LegacyApiHandler.ErrorToInternalErrorCode(err)
        logger := aplogger.CreateLogger()

        // 内部のエラーコードを元に、GateWayに関するエラーのエラーハンドリングする.
        switch iecd {
        // 内部処理エラー
        case errorcodes.xxxx:
            logger.WriteAppLog(aplogger.Error,
                fmt.Sprintf("gRPC access error. [%s] [%s]",
                    err.Error(), constant.GetProcessName(ctx)),
                ctx.Value(constant.TransactionIDContextKey), errorcodes.xxxx)

        case errorcodes.xxxx:
            logger.WriteAppLog(aplogger.Error,
                fmt.Sprintf("gRPC access timed out. [%s] [%s]",
                    err.Error(), constant.GetProcessName(ctx)),
                ctx.Value(constant.TransactionIDContextKey), errorcodes.xxxx)
        }
        return response, gatewayErr
    }
    return response, nil
}

以上で、gRPCクライアントとしての機能の実装は完了です。

ユースケースの実装

アプリケーション特有のビジネスロジックを実装します。
本例においては、「シーケンス」に記載したロジックが該当し、上記で実装した各種機能を利用して実現します。
usecase層に実装します。

usecase層

repositoryの実装

usecase -> repositoryディレクトリ内の実装を行います。
外側のレイヤーであるinterface層を実行するために、インターフェースを準備します。
user_database_repository.goはdatabaseのインターフェースを、user_client_repository.goはgatewayのインターフェースを実装します。

user_database_repository.go
package usecase

import "context"

// UserDatabaseRepository usecaseからinterfaceを実行するための、インタフェース.
type UserDatabaseRepository interface {
    Query(ctx context.Context, value interface{}, find interface{}, query interface{}, filters []interface{}, queryArgs ...interface{}) (interface{}, error)
}
user_client_repository.go
package usecase

import "context"

// UserClientRepository usecaseからinterfaceを実行するための、インタフェース.
type UserClientRepository interface {
    ListUsers(ctx context.Context, request model.User) (model.Users, error)
}

usecase層からinterface層を実行することができるようになりました。

serviceの実装

usecase -> serviceディレクトリ内の実装を行います。
ビジネスロジックを実装します。

user_service.go
package usecase

import (
    "context"
    "fmt"

    "github.com/relay-service/constant"
    "github.com/relay-service/errorcodes"

    aplogger "github.com/relay-service/common/logger"
    model "github.com/relay-service/domain/model"
    usecase "github.com/relay-service/usecase/repository"
)

// UserService サービスが利用するインタフェースの定義
type UserService struct {
    UserDatabaseRepository usecase.UserDatabaseRepository
    UserClientRepository usecase.UserClientRepository
}

// ListUserService ユーザ情報リスト取得
func (p *PayEnterpriseService) ListUserService(ctx context.Context, query model.User, filters []interface{}) ([]model.User, error) {

    logger := aplogger.CreateLogger()

    // リクエスト元ユーザの権限をチェックする.
    var Users []model.User
    if _, err := p.UserDatabaseRepository.Query(ctx, model.User{}, &users, query, filters); err != nil {
        // リクエスト元ユーザが存在しない、または権限不足の場合、エラー応答
        switch err.(type) {
        case errorcodes.NotFoundError:
            return nil, errorcodes.InternalErrorCodeToMyError(errorcodes.xxxx)
        }
    }

    // ユーザ情報の一覧を取得する.
    users, err := p.UserClientRepository.ListUsers(ctx, query)
    if err != nil {
        // エラーが発生した場合はエラーメッセージを応答する.
        return &users, err
    }

    // ユーザ情報の一覧を応答する.
    return &users, nil
}

以上で、ビジネスロジックの実装は完了です。
同時に全実装の完了です。

クリーンアーキテクチャ適用の所感と現場適用判断

筆者のスクラムチームでは、実際にクリーンアーキテクチャを適用した開発を行っています。
上述のとおり、クリーンアーキテクチャは様々なメリットがありますが、実際に適用してみた所感として以下のデメリットを感じています。

  • コード記述量が増大する
    • 各レイヤーを独立させるために、大量のインターフェースを準備する必要がある
  • 学習コストが高い
    • クリーンアーキテクチャの考え方を開発メンバが理解する必要があり、学習レベルが十分でないとメリットを100%享受することができない

筆者のスクラムチームでは10名程度の開発メンバが所属していますが、全メンバがクリーンアーキテクチャによる実装経験を踏むために、3ヶ月程度の期間を要しました。
幸いにも案件状況的に、比較的穏やかな期間があり、開発スケジュールに遅延を生じさせることなく適用させることができました。

各現場においては、以下のような基準で適用すべきかどうかの判断を慎重に行うことが肝要です。

  • 短期間の開発を求められるスケジュールであるか
    • 上述のとおり、適用においては開発チームの習熟が前提となり、コード記述量も増えるため短期開発での適用は避けるべきです
  • 開発メンバは十分に確保できているか
    • クリーンアーキテクチャではソースコードの独立性による並行開発で生産性が向上します。少数メンバによる開発においては、逆に生産性が低下する恐れがあります。
  • システム要件の追加や変更が生じる案件であるか
    • 仕様追加・仕様変更開発において、特にメリットを発揮することから、システム要件の変更が見込まれない案件においてはメリットを十分に享受できない可能性があります

まとめ

クリーンアーキテクチャはソースコードの独立性が高く、UIの追加や、インフラとなるデータベースの変更などがあった場合でも、変更レイヤーが明確であり、レイヤー間はインターフェースで依存しているため、一部レイヤーが変更になったとしても、他レイヤへの影響が極小化されます。
また、インターフェースを定義すれば各レイヤ毎に独立してコーディングとテストを行うことができ、十分な開発要員が確保できている前提において、スムーズな並行開発が実現でき生産性の向上が期待できます。
うまく活用することで、開発現場の様々な問題解決の糸口となることが期待できます。


  1. ドメインそのものとドメインのロジックに注目した考え方。Eric Evans 氏が2003年に著書で提唱。 

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10