【クリーンアーキテクチャの導入】Go言語でのAPI開発 | 実践的な学習から実装まで③
こんにちは!たけのこです。
今回は、前回まで実装してきたプロジェクトのディレクトリをクリーンアーキテクチャ設計手法に沿って整理をしていこうと思います。
この記事は、前回からの続きとなっていますので前回までの記事を読み進めてから本記事を読んでもらえると、より理解しやすくなると思います!
https://takenoko-blog.net/2024/04/07/go%e8%a8%80%e8%aa%9e%e3%81%a7%e3%81%aeapi%e9%96%8b%e7%99%ba-%e5%ae%9f%e8%b7%b5%e7%9a%84%e3%81%aa%e5%ad%a6%e7%bf%92%e3%81%8b%e3%82%89%e5%ae%9f%e8%a3%85%e3%81%be%e3%81%a7%e2%91%a1%e3%80%90%e7%b0%a1/
また、本記事は
- 前回の記事から引き続き、APIの実装を勉強している
- クリーンアーキテクチャの具体的なまとめ方を知りたい
といった方にオススメの記事となっています。
では早速、行きましょう!
クリーンアーキテクチャって何??
クリーンアーキテクチャについてザックリ説明すると、それぞれの機能やモジュールが分離している状態、つまりそれぞれのモジュールが独立して依存しない状態で開発する手法です。
クリーンアーキテクチャを導入するにあたって主なメリットとしては、
- 独立性が強化!
- コードテストがしやすい!
- システムの保守性が向上!
- システムの拡張がしやすい!
といった、4つがあります。
1つひとつ解説すると、冗長してしまうため「システムの保守・開発がしやすくなる手法」というフワッとした理解でOKです!
クリーンアーキテクチャに沿ってディレクト整理
では、前回まで実装していたAPIプロジェクトの内容をクリーンアーキテクチャに沿ってディレクトリの整理をしていきましょう!
下記のようなディレクトリ構成でプロジェクトを整理していきます。
myApiProject
┗cmd
┗main.go
┗internal
┗domain
┗user.go
┗usecase
┗user_usecase.go
┗handler
┗user_handler.go
┗repository
┗user_repository.go
┗infrastructure
┗database.go
┗pkg
┗router
┗router.go
cmd/main.goのソースコード
こちらではプロジェクトを実行した際に、ルーターとデータベースの初期化を行ない、サーバーを起動するような実装にしています。
プロジェクトを起動した際に一番最初に呼ばれるコンポーネントです!
package main
import (
"log"
"myApiProject/internal/infrastructure"
"myApiProject/pkg/router"
"net/http"
)
func main() {
db := infrastructure.InitDB()
defer db.Close()
router := router.NewRouter(db)
log.Fatal(http.ListenAndServe(":8080", router))
}
internal/domain/user.goのソースコード
こちらは、データベースで取り扱うデータを一時保持するための構造体を定義しています!いわゆる、エンティティの定義ですね~
package domain
type User struct {
UserID int
Name string
Email string
Password string
}
internal/usecase/user_usecase.goのソースコード
こちらはrepositoryパッケージを呼び出し、データの取得や登録、更新などを行なっています。主な役割としては、取得したデータの検証やビジネスルールの適用、トランザクションの管理などを行なっています!
handler層とrepository層の橋渡し的な存在です。
package usecase
import (
"myApiProject/internal/domain"
"myApiProject/internal/repository"
)
type UserUseCase struct {
repo repository.UserRepository
}
func NewUserUseCase(repo repository.UserRepository) *UserUseCase {
return &UserUseCase{repo: repo}
}
func (uc *UserUseCase) GetUser(userID string) (*domain.User, error) {
return uc.repo.FindByID(userID)
}
internal/handler/user_handler.goのソースコード
こちらでは、リクエストされた値を元にusecase層の処理を呼び出して適切なデータをレスポンスしています。
コードを見るとわかる通り、リクエストされた値がカラだったり、データの取得ができなかった場合にエラーを返す。適切なデータが取れた場合は、そのままデータをレスポンスするといった、API処理の管理職的な役割を担っています!
package handler
import (
"encoding/json"
"myApiProject/internal/usecase"
"net/http"
)
type UserHandler struct {
useCase *usecase.UserUseCase
}
func NewUserHandler(useCase *usecase.UserUseCase) *UserHandler {
return &UserHandler{useCase: useCase}
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
if userID == "" {
http.Error(w, "User ID is missing", http.StatusBadRequest)
return
}
user, err := h.useCase.GetUser(userID)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
internal/repository/user_repository.goのソースコード
こちらでは、usecase層から送られてきた値を元に、データの取得や登録、更新などを行ないます。
現状では、データ取得しかしていないので、単純にSELECT文を発行&実行を行なっています。
package repository
import (
"database/sql"
"myApiProject/internal/domain"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(userID string) (*domain.User, error) {
user := &domain.User{}
err := r.db.QueryRow("SELECT user_id, name, email, password FROM Users WHERE user_id = ?", userID).Scan(&user.UserID, &user.Name, &user.Email, &user.Password)
if err != nil {
return nil, err
}
return user, nil
}
internal/infrastructure/database.goのソースコード
こちらでは、データベースへの接続設定と、初期化処理を実装しています。
main.goの中身を見てみると、一番最初にInitDBクラスを呼び出してDB接続を行なうようにしていることがわかると思います!
package infrastructure
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func InitDB() *sql.DB {
db, err := sql.Open("mysql", "apiuser:apipassword@tcp(localhost:3306)/apidb")
if err != nil {
log.Fatal(err)
}
if err = db.Ping(); err != nil {
log.Fatal(err)
}
return db
}
pkg/http/router.goのソースコード
こちらでは、ルーティングの設定を行なっています。
いわゆる、URLの「http://localhost:8080/user?user_id=”任意のユーザーID”」部分の定義を行なっています。
package router
import (
"database/sql"
"myApiProject/internal/handler"
"myApiProject/internal/repository"
"myApiProject/internal/usecase"
"net/http"
)
func NewRouter(db *sql.DB) *http.ServeMux {
repo := repository.NewUserRepository(db)
useCase := usecase.NewUserUseCase(*repo)
userHandler := handler.NewUserHandler(useCase)
router := http.NewServeMux()
router.HandleFunc("/user", userHandler.GetUser)
return router
}
まとめ
という感じに、今回はディレクトリをクリーンアーキテクチャ構造に沿って整理してきました。
ザックリまとめると、それぞれのコンポーネントの役割は下記のようなイメージです!
myApiProject
┗cmd
┗main.go // ルーターとデータベースの初期化
┗internal
┗domain
┗user.go // エンティティの定義
┗usecase
┗user_usecase.go // データの検証やビジネスルールの適用
┗handler
┗user_handler.go // リクエストを処理し、適切なレスポンスを返答
┗repository
┗user_repository.go // データの取得や登録、更新などの実行
┗infrastructure
┗database.go // DBへの接続、初期化
┗pkg
┗router
┗router.go // ルーティング(URL)の定義
こう見ると、どこに何があるのか分かるのでシステムの開発や保守がやりやすくなることが分かりますね~
今後はクリーンアーキテクチャに沿ってAPIの実装について紹介していけたら、と思います。
ということで、今回はこの辺りで!ありがとうございました!