go

【クリーンアーキテクチャの導入】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の実装を勉強している
  • クリーンアーキテクチャの具体的なまとめ方を知りたい

といった方にオススメの記事となっています。

では早速、行きましょう!

スポンサーリンク

クリーンアーキテクチャって何??

クリーンアーキテクチャについてザックリ説明すると、それぞれの機能やモジュールが分離している状態、つまりそれぞれのモジュールが独立して依存しない状態で開発する手法です。

クリーンアーキテクチャを導入するにあたって主なメリットとしては、

  1. 独立性が強化!
  2. コードテストがしやすい!
  3. システムの保守性が向上!
  4. システムの拡張がしやすい!

といった、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の実装について紹介していけたら、と思います。

ということで、今回はこの辺りで!ありがとうございました!

スポンサーリンク
ABOUT ME
たけのこ
たけのこ
自由奔放エンジニア
現役でエンジニアをやっています! 開発現場で経験したコーディング実装例、実装アーキテクチャの解説などを記事に書き起こしています!
記事URLをコピーしました