· yuta

Go言語で実装して理解するクリーンアーキテクチャ:第1章 なぜアーキテクチャが必要なのか

#go#architecture#clean-architecture

Go言語でアプリケーションを実装しながら、クリーンアーキテクチャを学ぶシリーズを始めます。「クリーンアーキテクチャ」という言葉や同心円の図は見たことがあっても、「実際のGoのコードでは、どのパッケージに何を書けばいいのか」「なぜそこまでして層を分けるのか」がすっきりしない、という方は多いのではないでしょうか。このシリーズでは、1つのアプリケーションを実際に作り直していきながら、その答えを具体的なコードで確かめていきます。

初回はコードを書く前に、そもそもなぜアーキテクチャが必要なのかと、クリーンアーキテクチャの全体像を整理します。

変更に弱いコードの典型例

ハンドラにすべてを書いたコード

GoでWeb APIを書き始めると、最初は次のように、HTTPのハンドラの中にすべての処理を書くことが多いと思います。

func completeTask(w http.ResponseWriter, r *http.Request) {
    // HTTPのリクエストを読む
    userID := r.Header.Get("X-User-ID")

    // データベースから読む
    var t task
    err := db.QueryRowContext(r.Context(),
        `SELECT id, status FROM tasks WHERE id = $1 AND owner_id = $2`,
        r.PathValue("id"), userID).Scan(&t.ID, &t.Status)
    // ……エラー処理……

    // 業務のルール
    if t.Status == "done" {
        http.Error(w, "タスクは既に完了しています", http.StatusConflict)
        return
    }

    // データベースに書く
    _, err = db.ExecContext(r.Context(),
        `UPDATE tasks SET status = 'done', completed_at = $1 WHERE id = $2`, time.Now(), t.ID)
    // ……エラー処理とJSONのレスポンス……
}

小さなうちはこれで困りません。むしろ、処理が1か所にまとまっていて読みやすいとさえ言えます。ところが、アプリケーションが育つにつれて、次のような問題が出てきます。

何が困るのか

① テストが書きにくい

「完了済みのタスクはもう一度完了にできない」という、たった1行のルールを確かめたいだけなのに、テストにはデータベースが必要になります。time.Now() を直接呼んでいるので、完了日時が正しく入ったかどうかも確かめにくくなります。

② 変更の影響がどこまで及ぶかわからない

データベースをPostgreSQLから別のものに変えたい、あるいはテーブルの構造を変えたい、となったとき、SQLはすべてのハンドラに散らばっています。どのハンドラを直せばよいのかを、1つずつ探すことになります。

③ 業務のルールが埋もれる

「完了済みなら完了にできない」「タイトルは100文字まで」といった、このアプリケーションにとって最も大事なルールが、HTTPやSQLの処理の間に埋もれています。どこにどんなルールがあるのかを知るには、すべてのハンドラを読むしかありません。

④ 入口を増やせない

同じ処理をコマンドラインのツールや、定期実行のバッチからも使いたくなったとします。ルールがHTTPのハンドラの中にあるので、そのままでは再利用できず、コピーすることになります。コピーしたルールは、いずれ食い違っていきます。

これらの問題の根っこは同じです。変わる理由の違うもの(HTTP、SQL、業務のルール)が、1つの場所に混ざっていることです。アーキテクチャは、変わる理由ごとにコードを分け、それぞれの間の依存の向きを決めることで、変更しやすさとテストしやすさを保つための考え方です。

いくつかのアーキテクチャ

クリーンアーキテクチャは、突然生まれた考え方ではありません。先行するいくつかのアーキテクチャの共通点をまとめたものです。

レイヤードアーキテクチャ

最も広く知られているのが、プレゼンテーション層・ビジネスロジック層・データアクセス層のように、処理を役割ごとの層に分けるレイヤードアーキテクチャです。

レイヤードアーキテクチャの図。プレゼンテーション層がビジネスロジック層に、ビジネスロジック層がデータアクセス層に依存している

層に分けることで、先ほどの「すべてが混ざっている」問題はかなり改善します。ただし、依存の向きに注目すると、ビジネスロジック層がデータアクセス層に依存しています。つまり、アプリケーションで最も大事な業務のルールが、最も変わりやすい部分の1つであるデータベースの詳細に引きずられる形になっています。

ヘキサゴナルアーキテクチャ(ポートとアダプター)

Alistair Cockburnが提唱したヘキサゴナルアーキテクチャは、アプリケーションの中心と外の世界との間にポート(アプリケーションが決めたインターフェース)を置き、HTTPやデータベースといった外の世界との接続は、ポートに合わせて作るアダプターが担う、という考え方です。

ヘキサゴナルアーキテクチャの図。中心のアプリケーションの両側にポートがあり、HTTP・CLIのアダプターとPostgreSQL・メモリのアダプターがポートに向かって依存している

中心のアプリケーションは、自分が決めたポートしか知りません。データベースのアダプターを差し替えても、中心のコードは変わらない、というのがポイントです。

オニオンアーキテクチャ

Jeffrey Palermoが提唱したオニオンアーキテクチャは、ドメインモデルを中心に置き、その周りをドメインサービス、アプリケーションサービス、そして一番外側にUIやインフラストラクチャを置く、玉ねぎのような同心円の構造です。依存は必ず外側から内側に向かいます。

共通しているもの

呼び方や層の分け方は違っても、これらのアーキテクチャには共通点があります。

Robert C. Martin(通称Uncle Bob)は、2012年のブログ記事「The Clean Architecture」で、これらの共通点を1つの図にまとめました。これがクリーンアーキテクチャです。後に書籍『Clean Architecture』(日本語訳は『Clean Architecture 達人に学ぶソフトウェアの構造と設計』)で詳しく解説されています。

クリーンアーキテクチャの同心円

4つの円

クリーンアーキテクチャの図は、4つの同心円で表されます。

クリーンアーキテクチャの同心円の図。外側からFrameworks & Drivers、Interface Adapters、Use Cases、Entitiesの順に並び、依存の向きは外側から内側

円 役割 このシリーズでの例
Entities 業務の中核となるルールとデータ タスク。「完了済みのタスクはもう一度完了にできない」
Use Cases このアプリケーションに固有の処理の流れ 「自分のタスクを完了にする」
Interface Adapters 外の世界の形式と、内側の形式を変換する HTTPリクエストをユースケースの入力に変換する、SQLでタスクを保存する
Frameworks & Drivers フレームワーク、ドライバー、ツール net/http のサーバー、PostgreSQLのドライバー、設定の読み込み

内側ほど抽象的で変わりにくく、外側ほど具体的で変わりやすいものが置かれます。

依存性のルール

クリーンアーキテクチャで最も大切なのが、次の**依存性のルール(The Dependency Rule)**です。

ソースコードの依存は、内側に向かってだけ向かなければならない。

内側の円のコードは、外側の円のことを何も知ってはいけません。Goに当てはめると、「内側のパッケージは、外側のパッケージをimportしない」ということです。例えばエンティティのパッケージが net/http や database/sql をimportしていたら、ルール違反です。

境界を越えるときは依存を逆転させる

ここで疑問が湧くはずです。ユースケースは、タスクをデータベースに保存しなければなりません。データベースは外側にあるのに、どうやって外側を知らずに保存するのでしょうか。

答えは依存性逆転の原則です。ユースケースの側で「タスクを保存する」というインターフェースを定義し、ユースケースはそのインターフェースだけを使います。そして外側のアダプターが、そのインターフェースを実装します。

依存性逆転の図。Use CasesのCompleteTaskがTaskRepositoryインターフェースを使い、Interface Adaptersのpostgres.TaskRepositoryがそれを実装している

処理の流れ(呼び出し)はユースケースからデータベースへ向かいますが、ソースコードの依存(import)はデータベースのアダプターからユースケースへ向かいます。制御の流れと依存の向きを逆にできることが、依存性のルールを守りながらアプリケーションを組み立てる鍵です。Goのインターフェースは、実装する側が明示的に宣言しなくても満たせるので、この形がとても自然に書けます。

境界を越えるデータ

円の境界を越えて渡すデータは、内側の都合で決めた単純な構造体にします。例えばデータベースの行を表す構造体や、HTTPのリクエストの構造体を、そのままユースケースに渡してはいけません。渡してしまうと、内側がそれらの形に依存してしまうからです。

このシリーズで作るもの

タスク管理API

シリーズを通して、タスク(ToDo)を管理するREST APIを作ります。

メソッド パス 内容
POST /tasks タスクを作成する
GET /tasks 自分のタスクの一覧を取得する
GET /tasks/{id} 自分のタスクを1件取得する
POST /tasks/{id}/complete 自分のタスクを完了にする

機能は小さくても、次のような「アーキテクチャを考える価値のある」要素を含めています。

使う技術

用途 使うもの
言語 Go 1.25
HTTP 標準ライブラリの net/http(Go 1.22以降のルーティング機能)
データベース PostgreSQL 17、database/sql、pgx、sqlc
ログ 標準ライブラリの log/slog
テスト 標準の testing、gomock、testcontainers-go

Webフレームワークは使わず、できるだけ標準ライブラリで書きます。フレームワークの使い方ではなく、層の分け方と依存の向きに集中するためです。

サンプルコード

サンプルコードは、GitHubの y-sugiyama654/study-clean-architecture で公開しています。コードを書く第3〜13章は、章ごとに clean_arch_chapterXX というブランチを用意していて、その章が終わった時点のコードを確認できます。

シリーズの流れ

章 内容
第1章 なぜアーキテクチャが必要なのか(この記事)
第2章 前提となる設計原則(SOLID原則とGoのインターフェース)
第3章 まずはアーキテクチャなしで作ってみる
第4〜7章 同心円の内側から、1層ずつ実装する
第8〜12章 依存性の注入、トランザクション、横断的関心事、テスト戦略、パッケージ構成
第13章 変更に強いことを、実際に仕様を変えて確かめる
第14章 やりすぎを避けるために

第3章で、わざと「アーキテクチャなし」のコードを書くのがポイントです。以降の章で層を分けるたびに、何が良くなり、何が増えたのかを比べられるようにします。

まとめ

次回は、クリーンアーキテクチャの土台になっている設計原則、SOLID原則を、Goのコードで確認します。特に、依存性逆転の原則とGoのインターフェースの関係を詳しく見ていきます。