Slide 1

Slide 1 text

© - BASE, Inc. GoでのAPI開発現場の アーキテクチャ実装事例 2019.04.25 #突撃!!隣のアーキテクチャ - @hgsgtk

Slide 2

Slide 2 text

© - BASE, Inc. このトークについて • Go⾔語で実装するAPIアプリケーション • 変更容易性‧実装の統⼀性‧テスタビリティを⽬指し て試⾏錯誤した結果を晒す • 「XXXアーキテクチャやるぞ!」というスタートでは ない • Question‧Suggestionください

Slide 3

Slide 3 text

© - BASE, Inc. ⾃⼰紹介 東⼝和暉 (Kazuki Higashiguchi) Twitter / GitHub : @hgsgtk バックエンドエンジニア BASE BANK, Inc. / Dev Division

Slide 4

Slide 4 text

© - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

Slide 5

Slide 5 text

© - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

Slide 6

Slide 6 text

© - BASE, Inc. Layered“ish” Architecture + ポート‧アダプタ 実装したアーキテクチャ See also: ブログ 『Goを運⽤アプリケーションに導⼊する際 のレイヤ構造模索の旅路 | Go Conference Autumn 発表レポート』 https://devblog.thebase.in/entry/ / / / 書籍『実践ドメイン駆動設計』 https://www.shoeisha.co.jp/book/detail/

Slide 7

Slide 7 text

© - BASE, Inc. パッケージ構成 ※ パッケージ: 名前空間を分けるためのGoの仕組み

Slide 8

Slide 8 text

© - BASE, Inc. Domain Layer • Model‧Repository • Model • ビジネスロジック • Repository • Interfaceを定義

Slide 9

Slide 9 text

© - BASE, Inc. model packageの実装 package model import "time" type User struct { ID int db:"id"` FamilyName string db:"family_name" validate:"require,max=50"` GivenName string db:"given_name" validate:"require,max=50"` Created time.Time db:"created"` Modified time.Time db:"modified"` } func (u *User) GetFullName() string { return u.FamilyName + " " + u.GivenName }

Slide 10

Slide 10 text

© - BASE, Inc. model packageの実装 package model import "time" type User struct { ID int db:"id"` FamilyName string db:"family_name" validate:"require,max=50"` GivenName string db:"given_name" validate:"require,max=50"` Created time.Time db:"created"` Modified time.Time db:"modified"` } func (u *User) GetFullName() string { return u.FamilyName + " " + u.GivenName } User model を構造体として定義

Slide 11

Slide 11 text

© - BASE, Inc. repository packageの実装 package repository import ( "database/sql" "github.com/jmoiron/sqlx" ) type UserRepository interface { GetByID(db DBHandler, id int) (model.User, error) } // *sqlx.DB ߏ଄ମͷϨγʔόϝιουͷ͏ͪ࢖༻͢Δ΋ͷΛఆٛ type DBHandler interface { Exec // insert/update/delete ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Query // select ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Prepare // prepare ͷࡍʹ༻͍Δϝιουͷinterfaceܕ }

Slide 12

Slide 12 text

© - BASE, Inc. repository packageの実装 package repository import ( "database/sql" "github.com/jmoiron/sqlx" ) type UserRepository interface { GetByID(db DBHandler, id int) (model.User, error) } // *sqlx.DB ߏ଄ମͷϨγʔόϝιουͷ͏ͪ࢖༻͢Δ΋ͷΛఆٛ type DBHandler interface { Exec // insert/update/delete ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Query // select ͷࡍʹ༻͍Δϝιουͷinterfaceܕ Prepare // prepare ͷࡍʹ༻͍Δϝιουͷinterfaceܕ } interface型を定義 See also: https://tour.golang.org/methods/

Slide 13

Slide 13 text

© - BASE, Inc. Infrastructure Layer • datastore‧kvs etc • 「技術的実装」 • RDBMS (MySQL) • KVS (Redis) • APIClient • etc • Repository interfaceを 実装する

Slide 14

Slide 14 text

© - BASE, Inc. datastore package の実装 package datastore import ( // ུ ) type UserStore struct { } func (*UserStore) GetByID(db repository.DBHandler, id int) (user model.User, err error) { q := ` SELECT id, family_name, given_name, created, modified FROM users WHERE id = ?` st, err := db.Preparex(q) if err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } if err := st.QueryRowx(id).StructScan(&user); err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } return }

Slide 15

Slide 15 text

© - BASE, Inc. datastore package の実装 type UserStore struct { } func (*UserStore) GetByID(db repository.DBHandler, id int) (user model.User, err error) { q := ` SELECT id, family_name, given_name, created, modified FROM users WHERE id = ?` st, err := db.Preparex(q) if err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } if err := st.QueryRowx(id).StructScan(&user); err != nil { return user, errors.Wrap(err, "UserStore.GetByID got error") } return } UserRepository interfaceを満たす実装を⾏う

Slide 16

Slide 16 text

© - BASE, Inc. Application Layer • service package • 制御役 • ビジネスロジックを持た ない

Slide 17

Slide 17 text

© - BASE, Inc. service package の実装 package service import ( // লུ ) type UserViewService interface { Run(input UserViewInput) (UserViewOutput, *Error) } type UserViewInput struct { ID int } type UserViewOutput struct { User model.User } type Error struct { Code string Message string Status int Detail map[string]string }

Slide 18

Slide 18 text

© - BASE, Inc. type UserViewService interface { Run(input UserViewInput) (UserViewOutput, *Error) } type UserViewInput struct { ID int } type UserViewOutput struct { User model.User } type Error struct { Code string Message string Status int Detail map[string]string } service package の実装 ひとつのServiceごとにInterface型を定義

Slide 19

Slide 19 text

© - BASE, Inc. service package の実装 type UserViewServiceImpl struct { Conn repository.DBConnector User repository.UserRepository } func NewUserViewService(conn repository.DBConnector) *UserViewServiceImpl { return &UserViewServiceImpl{ Conn: conn, User: &datastore.UserStore{}, } } func (s *UserViewServiceImpl) Run(input UserViewInput) (UserViewOutput, *Error) { user, err := s.User.GetByID(s.Conn, input.ID) if err != nil { return UserViewOutput{}, &Error{ Code: "internal_server_error", Message: "Internal Server Error", Status: http.StatusInternalServerError, } } return UserViewOutput{User: user}, nil }

Slide 20

Slide 20 text

© - BASE, Inc. service package の実装 type UserViewServiceImpl struct { Conn repository.DBConnector User repository.UserRepository } func NewUserViewService(conn repository.DBConnector) *UserViewServiceImpl { return &UserViewServiceImpl{ Conn: conn, User: &datastore.UserStore{}, } } func (s *UserViewServiceImpl) Run(input UserViewInput) (UserViewOutput, *Error) { user, err := s.User.GetByID(s.Conn, input.ID) if err != nil { return UserViewOutput{}, &Error{ Code: "internal_server_error", Message: "Internal Server Error", Status: http.StatusInternalServerError, } } return UserViewOutput{User: user}, nil } UserViewService interface を満たす実装を⾏う

Slide 21

Slide 21 text

© - BASE, Inc. User Interface Layer • Controller‧Middleware • Controller • リクエストを処理し、 • service を呼び出し、 • レスポンスを作成する See also: middlewareの概念についての参考資料 https://book.cakephp.org/ . /en/controllers/ middleware.html

Slide 22

Slide 22 text

© - BASE, Inc. controller package の実装 package controller import ( // ུ ) type UserViewController struct { Service service.UserViewService } type UserViewRequest struct { ID int `json:"id"` } type UserViewResponse struct { ID int `json:"id"` FullName string `json:"full_name"` }

Slide 23

Slide 23 text

© - BASE, Inc. controller package の実装 func (c *UserViewController) Handler(w http.ResponseWriter, r *http.Request) { rb := UserViewRequest{} if err := decodeRequestBodyJSON(r, &rb); err != nil { respondError(w, err) return } output, err := c.Service.Run(service.UserViewInput{ID: rb.ID}) if err != nil { // respond error } rs := UserViewResponse{ ID: output.User.ID, FullName: output.User.GetFullName(), } respond(w, rs, http.StatusOK) }

Slide 24

Slide 24 text

© - BASE, Inc. controller package の実装 func (c *UserViewController) Handler(w http.ResponseWriter, r *http.Request) { rb := UserViewRequest{} if err := decodeRequestBodyJSON(r, &rb); err != nil { respondError(w, err) return } output, err := c.Service.Run(service.UserViewInput{ID: rb.ID}) if err != nil { // respond error } rs := UserViewResponse{ ID: output.User.ID, FullName: output.User.GetFullName(), } respond(w, rs, http.StatusOK) } serviceを実⾏する

Slide 25

Slide 25 text

© - BASE, Inc. お品書き 実装詳細 アーキテクチャ‧実装概要 まとめ

Slide 26

Slide 26 text

© - BASE, Inc. トランザクション制御 • databaseを操作する際に⽤いる database/sql package • See also: https://golang.org/pkg/database/sql/ • *sql.DB: database handle a pool of connections • ⼀度Openしたら基本的に使い続ける • 起動時に⽤意した *sql.DB を使う

Slide 27

Slide 27 text

© - BASE, Inc. トランザクション制御 • Application service 層で制御 • トランザクション制御のための connection handler を持つ package service type UserAddServiceImpl struct { Conn repository.DBConnector User repository.UserRepository }

Slide 28

Slide 28 text

© - BASE, Inc. トランザクション制御 • begin / commit / rollback func (s *UserAddServiceImpl) Run(input UserAddServiceInput) (UserAddServiceOutput, *Error) { tx, err := s.Conn.Beginx() if err != nil { // error handling } u := model.User{ FamilyName: input.FamilyName, GivenName: input.GivenName, } if err := s.User.Save(tx, u); err != nil { // error handling tx.Rollback() } tx.Commit() return UserAddServiceOutput{}, nil }

Slide 29

Slide 29 text

© - BASE, Inc. Extra: github.com/jmoiron/sqlx • sqlx is a library which provides a set of extensions on go’s standard database/sql library. • See also: https://github.com/jmoiron/sqlx • Merits • Marshal rows into structs

Slide 30

Slide 30 text

© - BASE, Inc. Test Double • 各レイヤ間では、 基本的にはInterface 型に依存 • それぞれのレイヤでのユニットテストでは、 Interface 型を実装した Test Doubleを作成し利⽤ • Test Doubleの作成には github.com/golang/mock を使⽤ • See also: https://github.com/golang/mock

Slide 31

Slide 31 text

© - BASE, Inc. Logging • 各構造体に持たせるかグローバルに定義したものを取 り出すか • グローバルで取り出せるロガーを定義して使⽤する

Slide 32

Slide 32 text

© - BASE, Inc. Logging package logger import ( // লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() }

Slide 33

Slide 33 text

© - BASE, Inc. Logging package logger import ( // লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } 起動時に、グローバルで取得するLoggerを初期化

Slide 34

Slide 34 text

© - BASE, Inc. Logging package logger import ( // লུ ) // Writer specifies output of logger. var Writer zapcore.WriteSyncer = os.Stdout // Init replace global zap logger to custom logger. func Init(output zapcore.WriteSyncer) { logger := newLogger(output) zap.ReplaceGlobals(logger) } // Logger return logger instance. func Logger() *zap.Logger { return zap.L() } Logger() によって、 グローバルLoggerを取り出して利⽤する

Slide 35

Slide 35 text

© - BASE, Inc. Logging: details package logger import ( // লུ ) func newLogger(writer zapcore.WriteSyncer) *zap.Logger { atom := zap.NewAtomicLevel() encoderCfg := zap.NewProductionEncoderConfig() encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder bl := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(encoderCfg), zapcore.Lock(writer), atom, )) l := bl.With(zap.String("out", "stdout")) return l }

Slide 36

Slide 36 text

© - BASE, Inc. BASEのプロダクトについて ネットショップ作成サービス 「BASE」 ショッピングアプリ 「BASE」 価値の交換をよりシンプルにし、 世界中の⼈々が最適な経済活動を⾏えるようにする。 MISSION

Slide 37

Slide 37 text

© - BASE, Inc.

Slide 38

Slide 38 text

Fin.