はじめに
この記事では、Go のクリーンアーキテクチャが読みにくくなりやすい理由を、技術駆動パッケージという観点から整理します。
結論から書くと、読みにくさの原因はクリーンアーキテクチャそのものより、handler usecase repository のように技術や責務の種類ごとにパッケージを切る構成にあります。
特に Go では、パッケージの切り方がそのままコードの見え方と依存の切れ方に直結します。
そのため、トップレベルで層ごとに分けると、機能のまとまりが想像以上に壊れやすくなることがあります。
この記事では次の点を扱います。
- 技術駆動パッケージとは何か
- なぜ Go では読みにくさが強く出やすいのか
- どのように切ると機能のまとまりを保ちやすいのか
技術駆動パッケージとは何か
技術駆動パッケージとは、業務機能ではなく、技術的な役割ごとにパッケージを分ける構成です。
例えば次のような形です。
handler/
usecase/
repository/
domain/
infrastructure/
一見すると整っています。
責務も明確に見えます。
ただし、これは「業務機能をどう扱うか」ではなく、「どういう種類のコードか」で整理している状態です。
例えばユーザー作成機能のコードは、次のように散らばります。
handler/user_handler.go
usecase/create_user.go
domain/user.go
repository/user_repository.go
infrastructure/mysql/user_repository.go
この時点で、ユーザー作成という 1 つの関心事が、技術的な都合で分断されています。
なぜこの構成は一見きれいに見えるのか
この構成が好まれやすい理由は分かります。
- 層の責務が説明しやすい
- クリーンアーキテクチャの図に対応づけやすい
- 初見では整理されて見える
- テンプレート化しやすい
つまり、構造の説明はしやすいです。
一方で、実装を読む側からすると重要なのは「この機能はどこを見れば分かるか」です。
そこが弱くなると、構造がきれいでも読みにくいコードになります。
Go ではなぜ読みにくさが強く出るのか
同じ技術駆動パッケージでも、Go では読みにくさが特に強く出やすいです。
理由は Go の package の性質にあります。
Go では基本的にディレクトリがパッケージ単位です。フォルダを分けることは見た目の整理ではなく、名前空間・依存方向・公開範囲をまたぐ境界そのものを増やすことを意味します。Go ではパッケージ分割の重みが比較的大きいです。
この性質が技術駆動パッケージと組み合わさると、次の問題が重なりやすいです。
例えば CreateUser という API を追う場合、次のような移動が発生します。
handler/user_handler.go
-> usecase/create_user.go
-> repository/user_repository.go(interface)
-> infrastructure/mysql/user_repository.go(実装)
-> domain/user.go
単純なユーザー作成処理でも、5 ファイルをまたぐことになります。機能単位で寄せた場合は user/ を順に読めばよく、ジャンプする回数が大きく減ります。
その結果、技術駆動パッケージでは次の状態になりやすいです。
- 変更量は小さい
- でも触るファイルは多い
- 実装の本体にたどり着くまで時間がかかる
- どこまでが本当に必要な抽象化か判断しづらい
また、パッケージをまたぐと、本来は閉じておきたい型や関数まで export に寄りやすくなります。同じ機能内だけで使いたい型でも、別パッケージに置いた瞬間に「export するか」「DTO を作るか」「パッケージ間でどう受け渡すか」を考える必要が出ます。
さらに、usecase と repository を別パッケージにした時点で interface を置きたくなりやすいです。
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
実装が 1 つしかない段階では、interface の追跡・実装の検索・DI の配線・モックの保守というコストが先に来ます。Go では「利用側が必要になったときに interface を定義する」のが自然なスタイルです。実装差し替えの要件が見えてから追加しても遅くありません。
Go の言語特性が、技術駆動パッケージの分断をさらに強める形になります。
機能単位で寄せると何がよいか
技術の種類ごとに切るより、機能のまとまりごとに寄せた方が読みやすくなることが多いです。
user/
handler.go
usecase.go
repository.go
model.go
order/
handler.go
usecase.go
repository.go
model.go
この形なら、user 機能を読むときに user/ を見ればよい、という状態を作りやすくなります。
2つの構成の違いは、図を分けて見る方が分かりやすいです。
機能単位でまとまる構成
この形なら、user/ を見ればユーザー機能の全体像が追えます。
技術駆動パッケージ
こちらは技術ごとに横断しており、1 機能を読むときに複数パッケージへ飛ぶ必要があります。
クリーンアーキテクチャと技術駆動パッケージは同じではない
ここは分けて考えた方がよいです。
クリーンアーキテクチャが求めているのは、依存方向の整理です。
必ずしも handler usecase repository をトップレベル package として並べることではありません。
例えば機能単位でまとめた上で、内部では依存方向を守ることもできます。
user/
handler.go
usecase.go
repository.go
entity.go
この構成でも、handler が usecase を呼び、usecase が repository を使うという依存方向は守れます。
つまり、読みにくさの原因をクリーンアーキテクチャ全体のせいにするより、パッケージの切り方の問題として見る方が正確です。
どんな場面でどちらが向くか
技術駆動パッケージのつらさが出やすいのは次の条件です。
- 小中規模の業務アプリ
- CRUD が多い
- 実装差し替えがあまりない
- 変更の多くが機能単位で発生する
この場合、最適化すべきなのは層の見た目より、変更時の追跡コストです。
逆に、次の条件なら役割ごとに強く分ける価値があります。
- ドメインルールが重い
- 外部連携や永続化の差し替えが現実的にある
- チームが大きく、責務境界を明示したい
ただしその場合でも、「本当に package を分ける必要があるか」は別途考えた方がよいです。
実務ではどう切るとよいか
自分なら、まず次の順で考えます。
- 変更は機能単位で起きるか
- その境界は package で分けるほど強いか
- 実装差し替えや公開範囲の制御が本当に必要か
この順で考えると、最初から技術駆動パッケージへ寄せすぎる失敗を減らしやすいです。
最初の構成としては、次のような形の方が扱いやすいことが多いです。
- 機能単位で package を切る
- package の中で handler/usecase/repository の役割を分ける
- interface は利用側が必要になったときに定義する
- 共通化は重複が痛くなってから行う
まとめ
Go のクリーンアーキテクチャが読みにくくなる理由は、クリーンアーキテクチャそのものより、技術駆動パッケージにあります。
特に Go では、package が単なるフォルダ整理ではなく、公開範囲と依存境界を持つため、技術ごとに切った分断がそのまま読みにくさにつながりやすいです。
問題になりやすいのは次の点です。
- 1 機能を追うのに複数 package をまたぐ
- package 分割により export や DTO が増えやすい
- interface と DI の追跡コストが上がりやすい
そのため、Go ではまず機能単位でまとめ、その中で依存方向を整える方が読みやすく保守しやすいことが多いです。
クリーンアーキテクチャは依存を整理する考え方です。
技術駆動パッケージは、その 1 つの実装方法にすぎません。
この 2 つを分けて考えるだけでも、設計の選択肢はかなり広がります。