Aller au contenu principal
友田 陽大
Go & Echo in production
Go
Echo
アーキテクチャ設計
型安全
テスト
保守性

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

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

Published
Reading time
10 min read
Author
友田 陽大
Partager

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

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

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


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
ControllerHTTP 入出力Echo・UseCaseEcho ハンドラ

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


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

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

// 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
}
// 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
}
// 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 の名の由来です。実装の詳細はデータベース層の記事で深掘りしています。


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

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

// 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)) // ③ 出力
}

入力検証はバインディング&バリデーション、エラーの HTTP 変換は集中エラーハンドラに寄せることで、Controller は本当に薄く保てます。これが SRP(単一責任)の具体形です。


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

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

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

//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型を辿って正しい順序で組み立てた初期化コードが生成されます。

// 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 にルートを登録するだけです。

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. ディレクトリ構成と循環インポート回避

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

cmd/server/main.go          # エントリポイント + wire
internal/
  domain/                   # エンティティ・interface・ドメインエラー(最内)
  usecase/                  # ユースケース(domain に依存)
  infra/                    # pgx 実装(domain の interface を実装)
  controller/               # Echo ハンドラ(usecase に依存)
  middleware/               # 横断的関心事
  • internal/ に置くと、外部モジュールから import されない(公開境界の明示)。
  • 循環インポートの回避:「内側は外側を import しない」を守れば循環は起きません。もし domaininfra を 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)——変更の影響を局所化すること。抽象が変更を難しくしているなら、それは間違った抽象です。

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


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

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

// 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 のテスト戦略で体系化しています。


まとめ:変更に強いバックエンドの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 本番運用ガイドへ。

友田

友田 陽大

Créateur d’un produit lauréat du prix du ministre (METI). Avec TypeScript + Python + AWS, je livre SaaS, transformation numérique industrielle et IA générative (RAG) prête pour la production — des spécifications à l’infrastructure et à l’exploitation, seul.

I can take on the implementation from this article as an engagement

I build Go / Echo backends, from design to production

API design and migration to Echo v5, clean architecture (Controller/UseCase/Repository + DI), middleware and security, centralized error handling, graceful shutdown, and testing/CI. With experience building a clean-architecture backend in Go/Echo + google/wire, I implement APIs that don't fall over, are traceable, and are easy to change.

Disponible en mission au forfait comme en conseil technique. Commencez par un échange gratuit de 30 minutes.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

À lire également