Go言語で実装して理解するクリーンアーキテクチャ:第1章 なぜアーキテクチャが必要なのか
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やデータベースといった外の世界との接続は、ポートに合わせて作るアダプターが担う、という考え方です。
中心のアプリケーションは、自分が決めたポートしか知りません。データベースのアダプターを差し替えても、中心のコードは変わらない、というのがポイントです。
オニオンアーキテクチャ
Jeffrey Palermoが提唱したオニオンアーキテクチャは、ドメインモデルを中心に置き、その周りをドメインサービス、アプリケーションサービス、そして一番外側にUIやインフラストラクチャを置く、玉ねぎのような同心円の構造です。依存は必ず外側から内側に向かいます。
共通しているもの
呼び方や層の分け方は違っても、これらのアーキテクチャには共通点があります。
- 業務のルールを中心に置き、フレームワークやデータベースといった技術的な詳細から切り離す
- 依存の向きを、詳細から中心に向ける
Robert C. Martin(通称Uncle Bob)は、2012年のブログ記事「The Clean Architecture」で、これらの共通点を1つの図にまとめました。これがクリーンアーキテクチャです。後に書籍『Clean Architecture』(日本語訳は『Clean Architecture 達人に学ぶソフトウェアの構造と設計』)で詳しく解説されています。
クリーンアーキテクチャの同心円
4つの円
クリーンアーキテクチャの図は、4つの同心円で表されます。
| 円 | 役割 | このシリーズでの例 |
|---|---|---|
| Entities | 業務の中核となるルールとデータ | タスク。「完了済みのタスクはもう一度完了にできない」 |
| Use Cases | このアプリケーションに固有の処理の流れ | 「自分のタスクを完了にする」 |
| Interface Adapters | 外の世界の形式と、内側の形式を変換する | HTTPリクエストをユースケースの入力に変換する、SQLでタスクを保存する |
| Frameworks & Drivers | フレームワーク、ドライバー、ツール | net/http のサーバー、PostgreSQLのドライバー、設定の読み込み |
内側ほど抽象的で変わりにくく、外側ほど具体的で変わりやすいものが置かれます。
依存性のルール
クリーンアーキテクチャで最も大切なのが、次の**依存性のルール(The Dependency Rule)**です。
ソースコードの依存は、内側に向かってだけ向かなければならない。
内側の円のコードは、外側の円のことを何も知ってはいけません。Goに当てはめると、「内側のパッケージは、外側のパッケージをimportしない」ということです。例えばエンティティのパッケージが net/http や database/sql をimportしていたら、ルール違反です。
境界を越えるときは依存を逆転させる
ここで疑問が湧くはずです。ユースケースは、タスクをデータベースに保存しなければなりません。データベースは外側にあるのに、どうやって外側を知らずに保存するのでしょうか。
答えは依存性逆転の原則です。ユースケースの側で「タスクを保存する」というインターフェースを定義し、ユースケースはそのインターフェースだけを使います。そして外側のアダプターが、そのインターフェースを実装します。
処理の流れ(呼び出し)はユースケースからデータベースへ向かいますが、ソースコードの依存(import)はデータベースのアダプターからユースケースへ向かいます。制御の流れと依存の向きを逆にできることが、依存性のルールを守りながらアプリケーションを組み立てる鍵です。Goのインターフェースは、実装する側が明示的に宣言しなくても満たせるので、この形がとても自然に書けます。
境界を越えるデータ
円の境界を越えて渡すデータは、内側の都合で決めた単純な構造体にします。例えばデータベースの行を表す構造体や、HTTPのリクエストの構造体を、そのままユースケースに渡してはいけません。渡してしまうと、内側がそれらの形に依存してしまうからです。
このシリーズで作るもの
タスク管理API
シリーズを通して、タスク(ToDo)を管理するREST APIを作ります。
| メソッド | パス | 内容 |
|---|---|---|
| POST | /tasks |
タスクを作成する |
| GET | /tasks |
自分のタスクの一覧を取得する |
| GET | /tasks/{id} |
自分のタスクを1件取得する |
| POST | /tasks/{id}/complete |
自分のタスクを完了にする |
機能は小さくても、次のような「アーキテクチャを考える価値のある」要素を含めています。
- タイトルの文字数や、「完了済みは完了にできない」といった業務のルール
- 他人のタスクは見えない、という認可のルール
- タスクの完了と操作履歴の記録を、1つのトランザクションで行う
- 認証、エラーのHTTPステータスコードへの変換、ログといった横断的な関心事
- 後から、データベースの差し替え、CLIの追加、完了時の通知を行う
使う技術
| 用途 | 使うもの |
|---|---|
| 言語 | 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章で、わざと「アーキテクチャなし」のコードを書くのがポイントです。以降の章で層を分けるたびに、何が良くなり、何が増えたのかを比べられるようにします。
まとめ
- ハンドラにすべてを書いたコードは、テストしにくく、変更の影響が読めず、業務のルールが埋もれ、別の入口から再利用できない
- その根本的な原因は、変わる理由の違うもの(HTTP、SQL、業務のルール)が1つの場所に混ざっていること
- レイヤード・ヘキサゴナル・オニオンの各アーキテクチャに共通する「業務のルールを中心に置き、依存を中心に向ける」という考え方をまとめたのが、クリーンアーキテクチャ
- クリーンアーキテクチャは Entities・Use Cases・Interface Adapters・Frameworks & Drivers の4つの円からなり、ソースコードの依存は内側にだけ向ける(依存性のルール)
- 外側の機能を使うときは、内側でインターフェースを定義して依存を逆転させる
次回は、クリーンアーキテクチャの土台になっている設計原則、SOLID原則を、Goのコードで確認します。特に、依存性逆転の原則とGoのインターフェースの関係を詳しく見ていきます。