0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで実践するクリーンアーキテクチャ:go-clean-archによる実用ガイド

Posted at

表紙

あなたが管理している Go プロジェクトのコードアーキテクチャはどのようなものですか?ヘキサゴナルアーキテクチャ(六角形アーキテクチャ)でしょうか?それともオニオンアーキテクチャ(玉ねぎアーキテクチャ)でしょうか?あるいは DDD(ドメイン駆動設計)でしょうか?プロジェクトがどのアーキテクチャを採用しているかに関係なく、コアの目的は常に同じであるべきです。それは、コードが理解しやすく、テストしやすく、保守しやすいことです。

本記事では、Uncle Bob による「クリーンアーキテクチャ(Clean Architecture)」を出発点として、そのコア思想を簡単に解説し、go-clean-arch リポジトリを例に、Go プロジェクトでこのアーキテクチャ思想をどのように実現するかを深く掘り下げます。

クリーンアーキテクチャ

クリーンアーキテクチャ(Clean Architecture)は、Uncle Bob が提唱したソフトウェアアーキテクチャの設計理念であり、レイヤー構造と明確な依存ルールによって、ソフトウェアシステムをより理解しやすく、テストしやすく、保守しやすくすることを目的としています。そのコア思想は関心の分離にあり、システム内のコアビジネスロジック(ユースケース)が実装の詳細(フレームワークやデータベースなど)に依存しないようにします。

Clean Architecture のコア思想は「独立性」です:

  • フレームワークからの独立: 特定のフレームワーク(Gin、GRPC など)に依存しません。フレームワークはあくまでツールであり、アーキテクチャの中心ではありません。
  • UI からの独立: ユーザーインターフェースは簡単に変更でき、システムの他の部分に影響を与えません。たとえば、Web UI をコンソール UI に置き換えても、ビジネスルールを変更する必要はありません。
  • データベースからの独立: データベース(例: MySQL から MongoDB への変更など)を切り替えても、コアビジネスロジックに影響を与えません。
  • 外部ツールからの独立: サードパーティのライブラリなどの外部依存は隔離され、システムのコアに直接影響しないようにします。

構造図

img

図のように、Clean Architecture は同心円(コンセントリックサークル)で表現され、それぞれの層が異なるシステムの責務を担っています:

  • コアエンティティ(Entities)

    • 位置:最内層
    • 責務:システムのビジネスルールを定義します。エンティティはアプリケーションの最もコアなオブジェクトであり、独立したライフサイクルを持っています。
    • 独立性:完全にビジネスルールに依存し、他の要素に依存しません。
  • ユースケース(Use Cases / Service)

    • 位置:エンティティ層のすぐ外側
    • 責務:アプリケーションのビジネスロジックを実装します。システム内のさまざまな操作(ユースケース)の流れを定義し、ユーザーの要求が満たされることを保証します。
    • 役割:ユースケースはエンティティ層を呼び出し、データの流れを調整し、応答を決定します。
  • インターフェースアダプタ(Interface Adapters)

    • 位置:さらに外側の層
    • 責務:外部システム(UI、データベースなど)のデータを内層で理解できる形式に変換します。また、コアビジネスロジックを外部システムで利用可能な形式に変換します。
    • 例:HTTP リクエストのデータを内部モデル(クラスや構造体など)に変換したり、ユースケースの出力データをユーザーに表示したりします。
    • 構成要素:コントローラー、ゲートウェイ(Gateways)、プレゼンター(Presenter)などが含まれます。
  • 外部フレームワークとドライバ(Frameworks & Drivers)

    • 位置:最外層
    • 責務:データベース、UI、メッセージキューなど外部世界とのやりとりを実現します。
    • 特徴:この層は内側の層に依存しますが、その逆は成立しません。この部分はシステムの中で最も変更しやすい部分です。

go-clean-arch プロジェクト

go-clean-arch は、Clean Architecture を実装した Go のサンプルプロジェクトです。このプロジェクトは 4 つのドメインレイヤー(Domain Layer)で構成されています:

Models Layer モデル層

役割:ドメインのコアデータ構造を定義し、プロジェクト内のビジネスエンティティ(例:記事、著者など)を記述します。

理論上の層:エンティティ層(Entities)に対応します。

例:

package domain

import (
        "time"
)

type Article struct {
        ID        int64     `json:"id"`
        Title     string    `json:"title" validate:"required"`
        Content   string    `json:"content" validate:"required"`
        Author    Author    `json:"author"`
        UpdatedAt time.Time `json:"updated_at"`
        CreatedAt time.Time `json:"created_at"`
}

Repository Layer リポジトリ層

役割:データソース(データベースやキャッシュなど)とのやりとりを担い、ユースケース層に統一されたインターフェースでデータアクセスを提供します。

理論上の層:外部フレームワークとドライバ層(Frameworks & Drivers)に対応します。

例:

package mysql

import (
        "context"
        "database/sql"
        "fmt"

        "github.com/sirupsen/logrus"

        "github.com/bxcodec/go-clean-arch/domain"
        "github.com/bxcodec/go-clean-arch/internal/repository"
)

type ArticleRepository struct {
        Conn *sql.DB
}

// NewArticleRepository will create an object that represent the article.Repository interface
func NewArticleRepository(conn *sql.DB) *ArticleRepository {
        return &ArticleRepository{conn}
}

func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) {
        rows, err := m.Conn.QueryContext(ctx, query, args...)
        if err != nil {
                logrus.Error(err)
                return nil, err
        }

        defer func() {
                errRow := rows.Close()
                if errRow != nil {
                        logrus.Error(errRow)
                }
        }()

        result = make([]domain.Article, 0)
        for rows.Next() {
                t := domain.Article{}
                authorID := int64(0)
                err = rows.Scan(
                        &t.ID,
                        &t.Title,
                        &t.Content,
                        &authorID,
                        &t.UpdatedAt,
                        &t.CreatedAt,
                )

                if err != nil {
                        logrus.Error(err)
                        return nil, err
                }
                t.Author = domain.Author{
                        ID: authorID,
                }
                result = append(result, t)
        }

        return result, nil
}

func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {
        query := `SELECT id,title,content, author_id, updated_at, created_at
                                                FROM article WHERE ID = ?`

        list, err := m.fetch(ctx, query, id)
        if err != nil {
                return domain.Article{}, err
        }

        if len(list) > 0 {
                res = list[0]
        } else {
                return res, domain.ErrNotFound
        }

        return
}

Usecase/Service Layer ユースケース/サービス層

役割:システムのコアアプリケーションロジックを定義し、ドメインモデルと外部とのやりとりの橋渡しをします。

理論上の層:ユースケース層(Use Cases / Service)に対応します。

例:

package article

import (
        "context"
        "time"

        "github.com/sirupsen/logrus"
        "golang.org/x/sync/errgroup"

        "github.com/bxcodec/go-clean-arch/domain"
)

type ArticleRepository interface {
        GetByID(ctx context.Context, id int64) (domain.Article, error)
}

type AuthorRepository interface {
        GetByID(ctx context.Context, id int64) (domain.Author, error)
}

type Service struct {
        articleRepo ArticleRepository
        authorRepo  AuthorRepository
}

func NewService(a ArticleRepository, ar AuthorRepository) *Service {
        return &Service{
                articleRepo: a,
                authorRepo:  ar,
        }
}

func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {
        res, err = a.articleRepo.GetByID(ctx, id)
        if err != nil {
                return
        }

        resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)
        if err != nil {
                return domain.Article{}, err
        }
        res.Author = resAuthor
        return
}

Delivery Layer 配信層

役割:外部からのリクエストを受け取り、ユースケース層を呼び出し、その結果を外部(HTTP クライアントや CLI ユーザーなど)に返却します。

理論上の層:インターフェースアダプター層(Interface Adapters)に対応します。

例:

package rest

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

        "github.com/bxcodec/go-clean-arch/domain"
)

type ResponseError struct {
        Message string `json:"message"`
}

type ArticleService interface {
        GetByID(ctx context.Context, id int64) (domain.Article, error)
}

// ArticleHandler  represent the httphandler for article
type ArticleHandler struct {
        Service ArticleService
}

func NewArticleHandler(e *echo.Echo, svc ArticleService) {
        handler := &ArticleHandler{
                Service: svc,
        }
        e.GET("/articles/:id", handler.GetByID)
}

func (a *ArticleHandler) GetByID(c echo.Context) error {
        idP, err := strconv.Atoi(c.Param("id"))
        if err != nil {
                return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())
        }

        id := int64(idP)
        ctx := c.Request().Context()

        art, err := a.Service.GetByID(ctx, id)
        if err != nil {
                return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
        }

        return c.JSON(http.StatusOK, art)
}

go-clean-arch プロジェクトのおおまかなコード構成は以下の通りです:

go-clean-arch/
├── internal/
│   ├── rest/
│   │   └── article.go           # Delivery Layer 配信層
│   ├── repository/
│   │   ├── mysql/
│   │   │   └── article.go       # Repository Layer リポジトリ層
├── article/
│   └── service.go               # Usecase/Service Layer ユースケース/サービス層
├── domain/
│   └── article.go               # Models Layer モデル層

go-clean-arch プロジェクトにおける各層間の依存関係は次のようになります:

  • ユースケース/サービス層はリポジトリインターフェースに依存しますが、その実装詳細については知りません。
  • リポジトリ層はインターフェースを実装しますが、これは外側のコンポーネントであり、ドメイン層のエンティティに依存しています。
  • 配信層(REST ハンドラーなど)はユースケース/サービス層を呼び出し、外部リクエストをビジネスロジックの呼び出しへと変換します。

この設計は依存性逆転の原則(Dependency Inversion Principle)に従い、コアビジネスロジックが外部の実装詳細から独立し、高いテスト性と柔軟性を持つことを保証します。

まとめ

本記事では、Uncle Bob のクリーンアーキテクチャ(Clean Architecture)と go-clean-arch サンプルプロジェクトを例に挙げて、Go プロジェクトにおいてクリーンアーキテクチャをどのように実現できるかを解説しました。コアエンティティ、ユースケース、インターフェースアダプター、外部フレームワークといった分層構造を通じて、関心の分離を明確にし、システムのコアビジネスロジック(Use Cases)と外部の実装詳細(フレームワーク、データベースなど)を疎結合にします。

go-clean-arch プロジェクトのアーキテクチャはレイヤーごとにコードを整理し、それぞれの層の責任が明確です:

  • モデル層(Domain Layer):コアビジネスエンティティを定義し、外部実装から独立。
  • ユースケース層(Usecase Layer):アプリケーションロジックを実装し、エンティティと外部のやりとりを調整。
  • リポジトリ層(Repository Layer):データ保存の具体的な詳細を実装。
  • 配信層(Delivery Layer):外部リクエストを処理し、結果を返却。

これはあくまで一例であり、実際のプロジェクトのアーキテクチャ設計は、実際の要件やチームの開発習慣、規範によって柔軟に調整するべきです。コアの目標は分層原則を守り、コードを理解しやすく、テストしやすく、保守しやすくすることであり、同時にシステムの長期的な拡張と進化を支援することです。


私たちはLeapcell、Goプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?