こちらは2022年に執筆した記事ですので、情報が古い可能性があります
はじめに
筆者について
業務系システムを開発している2年目エンジニア
個人開発したサービスについて
個人開発したサービスとそのリリースまでを以下の記事でまとめていますので、ぜひ見ていただけると幸いです。
バックエンドの技術スタック
採用したフレームワークやアーキテクチャは以下の通りです。
- Go 1.17.2
- Gin 1.7.7
- GORM 1.23
- クリーンアーキテクチャ
工夫した点
1. Goを採用
バックエンドの言語としてはGoを採用しました。
Goを採用した主な理由は以下通りです。
- 静的型付けで開発効率を上げることができる
- 文法がシンプルなので可読性をあげることができる
- マイクロサービス化が主流になってきている
Goの特徴を活かすことでバックエンド開発をスピーディーに進めることができた点が良かったです。
上記以外の特徴としては、Goはソースコードをコンパイルして1つのバイナリとして実行でき、イメージサイズを小さくすることができるので、RubyやPythonなどのインタプリタ言語に比べるとDockerとの相性が良いです。
そのため、コンテナを利用した開発〜デプロイまでを効率よく行えたと考えています。
また、ライブラリとそのナレッジが豊富で個人的に助かりました(笑)
2. フレームワークはGin/Gormを採用
Ginについて
WebフレームワークとしてはGinを採用しました。
Ginを採用した理由としては、その他フレームワーク「Echo」や「iris」に比べると歴史があり、ナレッジが豊富であったためです。また、特徴としては、軽量でHTTPルーターのレスポンスが速いのが良きでした。
Ginを採用することで、APIサーバーを簡単に実装できる上に、コード上で実装が追いやすくなりました。
ルーティングについて
Ginはgin.Deafalt()
で*gin.Engine
を生成します。
生成したgin.Engine
にHTTPリクエスト、第一引数にエンドポイント、第二引数に処理を定義することで、そのエンドポイントに各メソッドでアクセスした際の処理を書くことができます。
以下、ルーティングの例です。
import (
"github.com/gin-gonic/gin"
"main.go/adapter/gateway"
"main.go/adapter/presenter"
"main.go/infra/database"
"main.go/usecase/interactor"
)
type Routing struct {
DB *database.DB
Gin *gin.Engine
Port string
}
// ルーター初期化
func Init(db *database.DB) *Routing {
r := &Routing{
DB: db,
Gin: gin.Default(),
Port: ":80",
}
r.setRouting()
return r
}
// ルーティング設定
func (r *Routing) setRouting() {
// サウナ施設コントローラー
facilityController := controller.Facility{
InputFactory: interactor.NewFacilityInputPort,
OutputFactory: presenter.NewFacilityOutputPort,
FacilityRepo: gateway.NewFacilityRepository,
Conn: r.DB.Connection,
}
// サウナ施設情報
r.Gin.GET("/facility/:facilityID", facilityController.GetFacilityByID)
r.Gin.POST("/facilities/new", facilityController.CreateFacility)
r.Gin.DELETE("/facilities", facilityController.DeleteFacility)
r.Gin.PUT("/facilities/new", facilityController.CreateFacility)
}
設定したルーティングした後は、ポートを指定してサーバーを立ち上げます。
以下、サーバーを立ち上げる例です。
// Run サーバ処理開始
func (r *Routing) Run() {
r.Gin.Run(r.Port)
}
このようにGinを使用したルーティング処理を実装することでどのリソースに対する 何のリクエストなのか直感的に理解できる点が良かったです。
エラー特定について
GinはHTTP通信上で発生したエラーを収集できるエラー管理の特徴を備えているので、どのリクエストでどのエラーコードが起きたのかを出力してくれる点が良かったです。
以下、Ginが出力を行う例です。
[GIN] 2023/03/12 - 10:34:29 | 200 | 0.9401ms | ×××.××.×.× | GET "/facility/1"
[GIN] 2023/03/12 - 10:44:09 | 500 | 1.1805ms | ×××.××.×.× | GET "/facility/test"
後ほど説明しますが、GormのSQLログとの相性が良かったと思います。
GORMについて
DBとのやりとりにORMのGORMを採用しました。
GORMを採用した理由としては、バックエンドのモデル情報とDBのテーブル/カラム情報の紐付きを常に担保した状態で開発を進めることができるためです。
モデル情報とテーブル/カラム情報の紐付き
上記実現のために、開発中はGoのAPIサーバーを立ち上げる度にGORMのDBマイグレーションとテストデータ作成クエリが実行される仕組みを作りました。
モデル情報の定義によって、列名や型などの基本情報に加えて、1対多や多対多などのテーブル間のリレーションも定義することができます。
以下、バックエンド側のモデル情報とDBマイグレーションの例です。
package Domain
import (
"time"
)
// Facility 施設モデル
type Facility struct {
ID uint32 `gorm:"primaryKey" json:"id,omitempty"`
Name string `gorm:"not null" json:"name,omitempty"`
Address Address `json:"address,omitempty"`
Tel string `json:"tel,omitempty"`
EigyoStart string `json:"eigyo_start,omitempty"`
EigyoEnd string `json:"eigyo_end,omitempty"`
Price uint32 `json:"price,omitempty"`
WaterServerFlg string `gorm:"type:varchar(2)" json:"water_server_flg,omitempty"`
LodgingFlg string `gorm:"type:varchar(2)" json:"lodging_flg,omitempty"`
RestaurantFlg string `gorm:"type:varchar(2)" json:"restaurant_flg,omitempty"`
WorkingSpaceFlg string `gorm:"type:varchar(2)" json:"working_space_flg,omitempty"`
BooksFlg string `gorm:"type:varchar(2)" json:"books_flg,omitempty"`
HeatWaveFlg string `gorm:"type:varchar(2)" json:"heat_wave_flg,omitempty"`
AirBathFlg string `gorm:"type:varchar(2)" json:"air_bath_flg,omitempty"`
BreakSpaceFlg string `gorm:"type:varchar(2)" json:"break_space_flg,omitempty"`
Saunas []Sauna `json:"saunas,omitempty"`
WaterBaths []WaterBath `json:"water_baths,omitempty"`
Articles []Article `json:"articles,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at,omitempty"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"`
}
// DBMigrate DBマイグレーションを行う
func (d *DB) DBMigrate() {
err := d.Connection.Migrator().DropTable(Domain.Facility{})
fmt.Println("delete: ", err)
err = d.Connection.AutoMigrate(Domain.Facility{})
fmt.Println("migrate: ", err)
fmt.Println("----db migrate done----")
}
作成されたテーブル情報
No | 列名 | データ・タイプ |
---|---|---|
1 | id | bigserial |
2 | name | text |
3 | tel | text |
4 | eigyo_star | text |
5 | eigyo_end | text |
6 | price | int8 |
7 | water_server_flg | varchar(2) |
8 | lodging_flg | varchar(2) |
9 | restaurant_flg | varchar(2) |
10 | working_space_flg | varchar(2) |
11 | books_flg | varchar(2) |
12 | heat_wave_flg | varchar(2) |
13 | air_bath_flg | varchar(2) |
14 | break_space_flg | varchar(2) |
15 | created_at | timestamptz |
16 | updated_at | timestamptz |
細かな設定はGORMドキュメントを参考に設定しました。
SQLについて
GORMを使用することでGoでSQL処理を簡単に短く書くことができる点とSQLログを簡単に出力できる点が良かったです。
業務でSQLを書くことが多いので、SQLの知識を活用しつつ、実装ができました。
ただ、複雑なSQLを書くことができない点がGORMやその他ORMを使用する際のデメリットです。
なので、実装する処理の大きさや複雑さによってORMの採用は控えた方が良いかなと思いました。
また、SQLの知識がない状態でORMを採用するのは個人的にあまりおすすめできません。
以下は、GORMを使用したSQL実装の例と出力されたログの一例です。
ログは、*gorm.DB
に定義されたDebug
を呼び出すだけで出力することが可能です。
- 定義方法
if err := conn.Debug().Table("facility").
Select("facility.id,facility.name").
Where("facility.id=?", facilityID).
First(&facility).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("サウナ情報が見つかりませんでした。")
}
return nil, fmt.Errorf("サウナ名取得に失敗しました。")
}
- 出力されたSQLログ
2023/03/12 10:13:04 /backend/src/adapter/gateway/facility.go:162
[13.429ms] [rows:1] SELECT facility.id,facility.name FROM "facility" WHERE facility.id='1' ORDER BY "facility"."id" LIMIT 1
[GIN] 2023/03/12 - 10:13:04 | 200 | 15.204ms | ×××.××.×.× | GET "/facility/1/facilityName"
実行されたSQLとその結果処理された行数や実行時間がわかる点が非常に便利でした。
Ginのログと組み合わせることで開発効率を上げることができました。
3. クリーンアーキテクチャを採用
バックエンドのアーキテクチャにはクリーンアーキテクチャを採用しました。
(とは言いつつも、クリーンアーキテクチャを完全に理解できた自信はありません。。)
クリーンアーキテクチャを採用した理由としては、レイヤーごとの責務とコードの依存方向を意識した実装を行うためです。
レイヤーごとに責務が分かれているアーキテクチャは、データの流れが掴みやすいので、改修がしやすく、バグの調査もしやすいことがメリットです。(業務で扱っているコードの反省を活かしています)
また、依存方向を徹底することでテストがしやすくなることもメリットです。
レイヤー
今回は4つのレイヤーに分けて実装しました。
No | レイヤー | 責務 |
---|---|---|
1 | インフラ層 | DB接続やルーティングを定義 |
2 | ユースケース層 | インタフェースを定義 |
3 | アダプター層 | DB操作やHTTPリクエストの入出力を定義 |
4 | ドメインモデル層 | ドメインロジックを定義 |
依存性の方向
ディレクトリ構成
ディレクトリ構成(一部抜粋)は以下の通りです。
└── src
├── adapter
│ ├── controller
│ │ └── facility.go
│ ├── gateway
│ │ └── facility.go
│ └── presenter
│ └── facility.go
├── go.mod
├── go.sum
├── infra
│ ├── database
│ │ ├── config.go
│ │ └── db.go
│ └── server
│ └── router.go
├── main.go
├── middleware
│ └── JWTAuth.go
├── model
│ ├── Domain
│ │ └── facility.go
│ └── ValueObject
│ └── facility.go
└── usecase
├── interactor
│ └── faclity.go
└── port
└── facility.go
デメリット
デメリットとしては、以下の通りです。
- コード量が多くなってしまう点
- 直感的なコードにならない点
クリーンアーキテクチャの採用はコードの規模に合わせて考えるべきだと感じました。
クリーンアーキテクチャは以下の記事を参考にしました。
今後どうしていきたいか/学ぶべきこと
Goを使用したバックエンド開発を進めましたが、触れられていない技術は多々あります。
特に以下のことを学びつつ、個人開発(バックエンド)に適用できればなと考えています。
- 並列処理について
- テスト
どうやって学習したのか(おまけ)
以下の教材を使用して学習を進めました。
どの教材も基礎的な内容から濃い内容まで網羅できたのでおすすめです。
一度で理解しようとせずに、個人開発しながら知識を定着させていけたのは良かったと思います。
Go
GORM
クリーンアーキテクチャ
RESTAPI
まとめ
個人開発のバックエンド編を振り返りました。
Goやクリーンアーキテクチャを使用して開発していきたい方に参考になれば幸いです。
触れられていない技術は多々あるので、精進していこうと思いました!