# Echo でクリーンアーキテクチャ + DI（google/wire）：ハンドラを薄く保ち、変更とテストに強いバックエンドを作る

> Go Echo（v5）でクリーンアーキテクチャと依存性注入（google/wire）を実装するガイド。Controller/UseCase/Repository の層分割、依存性逆転（内向きの依存ルール）、インターフェースの置き場所、wire によるコンパイル時 DI、ディレクトリ構成、循環インポート回避、過剰設計を避ける KISS/YAGNI の線引きまでを、実プロジェクトの知見と実コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Go, Echo, アーキテクチャ設計, 型安全, テスト, 保守性
- URL: https://tomodahinata.com/blog/go-echo-clean-architecture-dependency-injection-google-wire-guide
- カテゴリ: Go・Echo 本番運用
- 総合ガイド: https://tomodahinata.com/blog/go-echo-framework-production-guide

## 要点

- 依存は常に内向き。Controller→UseCase→Repository（interface）で、ビジネスロジックは HTTP も DB も知らない純粋な層に隔離する
- インターフェースは実装側ではなく利用側（UseCase）に置く。これで Repository を差し替え可能にし、UseCase を DB なしで単体テストできる
- Echo ハンドラは Controller。Bind/Validate して UseCase を呼び結果を JSON にするだけの薄い層に保つ
- 依存の配線は google/wire でコンパイル時に解決。手書きの new 地獄と実行時の nil 注入ミスを消す
- 小さなアプリに4層は過剰。KISS/YAGNI で層は必要になってから足す。原則の目的は『変更の影響を局所化する』こと

---

「最初は綺麗だったのに、半年で誰も触りたくないコードになった」——その分岐点は、たいてい**ハンドラにビジネスロジックを書き始めた瞬間**です。Echo ハンドラに DB 呼び出しと業務ルールと HTTP の体裁が混ざると、テストは書けず、変更は怖くなり、変更の影響が読めなくなります。

この記事は、[Go Echo 本番運用ガイド](/blog/go-echo-framework-production-guide)のアーキテクチャ編です。私が実際に [Go/Echo + google/wire でバックエンドを構築した飲食店マッチングサイト](/case-studies/restaurant-matching)で採った、**クリーンアーキテクチャ + 依存性注入（DI）**の設計を、「なぜそうするか」から実装まで解説します。目的は流行りの構造を真似ることではなく、**変更とテストに強い**コードベースを作ることです。

> **この記事のルール**：Echo の API は **公式ドキュメント（v5・2026年6月時点）** に基づきます。`google/wire` は更新されるため公式で最新を確認してください。本記事は設計指針が中心で、**過剰設計（over-engineering）を避ける線引き**（[第7章](#7-過剰にしないkissyagniで層は必要になってから)）まで含めて、実プロジェクトで使える形にしています。

---

## 1. たった一つの原則：依存は内向きにしか向かない

クリーンアーキテクチャの本質は、難しい図ではなく**1つのルール**です。

> **依存の方向は、常に外側から内側へ。内側は外側を知らない。**

層を内側から並べると：

```
[ Domain ]  ← 業務の中核（エンティティ・ビジネスルール）。何にも依存しない
   ↑
[ UseCase ] ← アプリのユースケース。Domain に依存。HTTP も DB も知らない
   ↑
[ Controller(Echo) ] / [ Repository(pgx) ] ← 外側。UseCase に依存する
```

- **Domain / UseCase（内側）**：HTTP・DB・Echo・pgx を**一切 import しない**。純粋な Go。
- **Controller / Repository（外側）**：フレームワークやドライバを知る、付け替え可能な「詳細」。

この向きさえ守れば、**「Web フレームワークを Echo から変える」「DB を PostgreSQL から変える」が内側に波及しません**。Echo はあくまで**最も外側の詳細**であって、アプリの中心ではない——これが設計の出発点です。

---

## 2. 層の責務：4つのレイヤーを1分で

| 層 | 責務 | 知ってよいもの | 例 |
| --- | --- | --- | --- |
| **Domain** | エンティティ・不変条件 | 何も（純粋 Go） | `User`, `Order`, ドメインエラー |
| **UseCase** | ユースケースの手順 | Domain と Repository の**interface** | 「ユーザー登録」「注文確定」 |
| **Repository** | 永続化の実装 | DB ドライバ（pgx 等） | `pgUserRepository` |
| **Controller** | HTTP 入出力 | Echo・UseCase | Echo ハンドラ |

ポイントは、**UseCase が Repository の「インターフェース」にだけ依存**し、その**実装（pgx）を知らない**ことです。次章の「依存性逆転」がこれを成立させます。

---

## 3. 依存性逆転：インターフェースは「利用側」に置く

初学者がつまずく最大のポイントがここです。**Repository のインターフェースは、実装側（インフラ）ではなく、利用側（UseCase/Domain）に置きます。**

```go
// domain/user.go ── 内側がインターフェースを「要求」する
package domain

type User struct {
	ID    string
	Name  string
	Email string
}

// UseCase が必要とする操作を、内側で定義する（実装は知らない）
type UserRepository interface {
	FindByID(ctx context.Context, id string) (*User, error)
	Create(ctx context.Context, u *User) error
}
```

```go
// usecase/user_usecase.go ── インターフェースにのみ依存
package usecase

type UserUseCase struct {
	repo domain.UserRepository // ← 具象 pgx ではなく interface
}

func NewUserUseCase(repo domain.UserRepository) *UserUseCase {
	return &UserUseCase{repo: repo}
}

func (uc *UserUseCase) Register(ctx context.Context, name, email string) (*domain.User, error) {
	// ここは純粋な業務ロジックだけ。HTTP も SQL も無い
	if email == "" {
		return nil, domain.ErrInvalidEmail
	}
	u := &domain.User{ID: newID(), Name: name, Email: email}
	if err := uc.repo.Create(ctx, u); err != nil {
		return nil, err
	}
	return u, nil
}
```

```go
// infra/pg_user_repository.go ── 外側が内側の interface を「実装」する
package infra

type pgUserRepository struct {
	pool *pgxpool.Pool
}

// 戻り値の型は domain.UserRepository（依存性逆転の成立点）
func NewUserRepository(pool *pgxpool.Pool) domain.UserRepository {
	return &pgUserRepository{pool: pool}
}
```

矢印が反転していることに注目してください。普通なら「UseCase → pgx」と外向きに依存しそうなところを、**interface を内側に置くことで「pgx → UseCase の契約」へと逆転**させています。これが Dependency **Inversion** の名の由来です。実装の詳細は[データベース層の記事](/blog/go-echo-database-postgresql-pgx-sqlc-gorm-transaction-guide)で深掘りしています。

---

## 4. Controller：Echo ハンドラは「薄い変換器」に保つ

Echo ハンドラ（Controller）の仕事は3つだけ。**①入力を Bind/Validate、②UseCase を呼ぶ、③結果を JSON にする**。業務ロジックは1行も書きません。

```go
// controller/user_handler.go
package controller

type UserHandler struct {
	uc *usecase.UserUseCase // UseCase にのみ依存
}

func NewUserHandler(uc *usecase.UserUseCase) *UserHandler {
	return &UserHandler{uc: uc}
}

func (h *UserHandler) Create(c *echo.Context) error {
	var dto CreateUserDTO
	if err := c.Bind(&dto); err != nil { // ① 入力（境界）
		return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
	}
	if err := c.Validate(&dto); err != nil {
		return err
	}
	user, err := h.uc.Register(c.Request().Context(), dto.Name, dto.Email) // ② 委譲
	if err != nil {
		return err // 集中エラーハンドラが HTTP に変換
	}
	return c.JSON(http.StatusCreated, toResponse(user)) // ③ 出力
}
```

入力検証は[バインディング＆バリデーション](/blog/go-echo-request-binding-validation-error-handling-guide)、エラーの HTTP 変換は[集中エラーハンドラ](/blog/go-echo-request-binding-validation-error-handling-guide#5-エラーハンドリング握りつぶさずreturnし一箇所で整形する)に寄せることで、Controller は本当に薄く保てます。これが SRP（単一責任）の具体形です。

---

## 5. google/wire：依存の配線をコンパイル時に解決する

層を分けると、`main` での**配線（new の連鎖）**が増えます。手書きすると、引数の順番ミスや nil 注入が**実行時**に初めて露見します。**google/wire** は、この配線を**コンパイル時にコード生成**で解決します（リフレクションなし＝実行時コストゼロ）。

各層に「プロバイダ関数」（`NewXxx`）を用意し、wire に組み立てを宣言します。

```go
//go:build wireinject
// +build wireinject

// wire.go ── これは「設計図」。wire がここから実体コードを生成する
package main

import "github.com/google/wire"

func InitializeUserHandler(pool *pgxpool.Pool) *controller.UserHandler {
	wire.Build(
		infra.NewUserRepository,   // *pgxpool.Pool      → domain.UserRepository
		usecase.NewUserUseCase,    // domain.UserRepository → *usecase.UserUseCase
		controller.NewUserHandler, // *usecase.UserUseCase  → *controller.UserHandler
	)
	return nil // 本体は wire が生成（このreturnはダミー）
}
```

`wire` コマンドを実行すると、`wire_gen.go` に**型を辿って正しい順序で組み立てた**初期化コードが生成されます。

```go
// wire_gen.go（自動生成）── 手書きの配線がこれに置き換わる
func InitializeUserHandler(pool *pgxpool.Pool) *controller.UserHandler {
	userRepository := infra.NewUserRepository(pool)
	userUseCase := usecase.NewUserUseCase(userRepository)
	userHandler := controller.NewUserHandler(userUseCase)
	return userHandler
}
```

`main` は生成された関数を呼び、Echo にルートを登録するだけです。

```go
func main() {
	pool, _ := infra.NewPool(context.Background(), os.Getenv("DATABASE_URL"))
	defer pool.Close()

	userHandler := InitializeUserHandler(pool) // wire 生成の配線
	e := echo.New()
	e.POST("/api/v1/users", userHandler.Create)
	// ...グレースフルに起動（StartConfig）
}
```

> **wire の利点**：依存が増えても**設計図に1行足すだけ**。型が合わなければ**生成時に落ちる**ので、nil 注入のような実行時バグが原理的に起きません。プロバイダをまとめる `wire.NewSet`、インターフェースと実装を結ぶ `wire.Bind` を使えば、機能単位での再利用も効きます。

---

## 6. ディレクトリ構成と循環インポート回避

層を物理的にパッケージで分け、**依存の向きをパッケージ境界で強制**します。

```text
cmd/server/main.go          # エントリポイント + wire
internal/
  domain/                   # エンティティ・interface・ドメインエラー（最内）
  usecase/                  # ユースケース（domain に依存）
  infra/                    # pgx 実装（domain の interface を実装）
  controller/               # Echo ハンドラ（usecase に依存）
  middleware/               # 横断的関心事
```

- **`internal/`** に置くと、外部モジュールから import されない（公開境界の明示）。
- **循環インポートの回避**：「内側は外側を import しない」を守れば循環は起きません。もし `domain` が `infra` を import したくなったら、**設計が間違っているサイン**——その依存は interface 経由に直します。
- **DTO の置き場所**：HTTP の入出力 DTO は `controller` に置き、`domain` には漏らしません（Web の都合をドメインに持ち込まない）。

---

## 7. 過剰にしない：KISS/YAGNI で層は必要になってから

ここが最も重要かつ、語られないポイントです。**全アプリにこの4層は要りません。**

- **小さな CRUD・短命なツール**：Controller から直接 Repository を呼んでよい。UseCase 層が「ただの素通し」なら、それは YAGNI 違反（不要な抽象）です。
- **層を足す判断基準**：「同じ業務ロジックを複数の入口（HTTP・バッチ・gRPC）から呼ぶ」「ロジックが分岐して肥大化してきた」「テストで DB を切り離したい」——**痛みが出てから**層を足します。
- **原則の目的を忘れない**：DRY・SRP・依存性逆転は**手段**であって目的ではありません。目的は[ETC（Easier To Change）](/blog/spec-driven-development-claude-code-ai-agent-production-workflow)——**変更の影響を局所化すること**。抽象が変更を**難しく**しているなら、それは間違った抽象です。

> 私が飲食店マッチングのバックエンドで4層 + wire を採ったのは、**チーム開発**でテスタビリティと並行作業を両立する必要があったからです。一人で書く小さな API なら、もっと薄くします。**規模と体制に合わせて層を選ぶ**のが、原則を「正しく」使うということです。

---

## 8. 見返り：テスト容易性という最大の報酬

この設計の対価は「ファイル数の増加」、見返りは**テスト容易性**です。UseCase は interface にしか依存しないので、**モックを注入すれば DB なしで全分岐を単体テスト**できます。

```go
// Repository をモックに差し替え、UseCase をDBなしでテスト
type mockUserRepo struct {
	createFn func(ctx context.Context, u *domain.User) error
}
func (m *mockUserRepo) Create(ctx context.Context, u *domain.User) error { return m.createFn(ctx, u) }
func (m *mockUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) { return nil, nil }

func TestRegister_DuplicateEmail(t *testing.T) {
	uc := usecase.NewUserUseCase(&mockUserRepo{
		createFn: func(ctx context.Context, u *domain.User) error { return domain.ErrEmailTaken },
	})
	_, err := uc.Register(context.Background(), "Hinata", "dup@example.com")
	assert.ErrorIs(t, err, domain.ErrEmailTaken)
}
```

DB を立てる統合テストと、こうした高速な単体テストを使い分ける戦略は、[Echo のテスト戦略](/blog/go-echo-testing-strategy-httptest-echotest-testcontainers-guide)で体系化しています。

---

## まとめ：変更に強いバックエンドの7原則

1. **依存は内向きにしか向かわない**。Domain/UseCase は HTTP も DB も知らない。
2. **インターフェースは利用側（UseCase）に置く**＝依存性逆転で実装を差し替え可能に。
3. **Echo ハンドラは薄い Controller**。Bind/Validate→委譲→JSON だけ。
4. **配線は google/wire でコンパイル時 DI**。new 地獄と nil 注入を消す。
5. **`internal/` で層をパッケージ分割**し、依存の向きを境界で強制。
6. **循環インポートは設計ミスのサイン**。interface 経由に直す。
7. **KISS/YAGNI で層は痛みが出てから**。原則の目的は ETC（変更の局所化）。

クリーンアーキテクチャは「正しい図を描くこと」ではなく、「**変更が来たときに、どれだけ狭い範囲の修正で済むか**」で価値が決まります。Echo の薄さは、この設計を Go の言語機能（interface・パッケージ）だけで自然に実現できることの裏返しです。全体像は[Go Echo 本番運用ガイド](/blog/go-echo-framework-production-guide)へ。
