Go言語のMVCとDDDレイヤードアーキテクチャの詳細な比較
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go言語のMVCとDDDレイヤードアーキテクチャの詳細な比較
MVCとDDDは、バックエンド開発で一般的な2つのレイヤードアーキテクチャの概念です。MVC(Model-View-Controller)は、主にユーザーインターフェース、ビジネスロジック、データモデルを分離し、疎結合化とレイヤリングを容易にするためのデザインパターンです。一方、DDD(ドメイン駆動設計)は、ビジネスドメインモデルを構築することにより、複雑なシステムの設計と保守の困難さを解決することを目的としたアーキテクチャ手法です。
Javaのエコシステムでは、多くのシステムが徐々にMVCからDDDに移行してきました。しかし、Go、Python、NodeJSなどの、シンプルさと効率を重視する言語では、MVCが主流のアーキテクチャのままです。以下では、Go言語に基づくMVCとDDDのディレクトリ構造の違いについて具体的に説明します。
MVCの図構造
+------------------+
| View | ユーザーインターフェース層:データの表示とユーザーインタラクションを担当(HTMLページ、APIレスポンスなど)
+------------------+
| Controller | コントローラー層:ユーザーリクエストを処理し、サービスロジックを呼び出し、モデルとビューを調整
+------------------+
| Model | モデル層:データオブジェクト(データベースのテーブル構造など)と一部のビジネスロジックを含む(サービス層に分散することが多い)
+------------------+
DDDの図構造
+--------------------+
| User Interface | ユーザーインタラクションと表示を担当(REST API、Webインターフェースなど)
+--------------------+
| Application Layer | ビジネスプロセスを調整(ドメインサービスの呼び出し、トランザクション管理など)、コアビジネスルールは含まない
+--------------------+
| Domain Layer | コアビジネスロジック層:集約ルート、エンティティ、値オブジェクト、ドメインサービスなどを含み、ビジネスルールをカプセル化
+--------------------+
| Infrastructure | 技術的な実装を提供(データベースアクセス、メッセージキュー、外部APIなど)
+--------------------+
MVCとDDDの主な違い
-
コード構成ロジック
- MVCは、技術的な機能(Controller/Service/DAO)でレイヤー化し、技術的な実装に焦点を当てます。
- DDDは、ビジネスドメイン(注文ドメイン、支払いドメインなど)でモジュールを分割し、境界づけられたコンテキストを通じてコアビジネスロジックを分離します。
-
ビジネスロジックのキャリア
- MVCは通常、貧血モデルを採用し、データ(Model)と動作(Service)を分離するため、ロジックが分散し、メンテナンスコストが高くなります。
- DDDは、集約ルートとドメインサービスを通じてリッチモデルを実現し、ビジネスロジックをドメイン層に集中させ、スケーラビリティを高めます。
-
適用性とコスト
- MVCは開発コストが低く、要件が安定している中小規模のシステムに適しています。
- DDDは、事前のドメインモデリングと統一された言語を必要とし、複雑なビジネスと長期的な進化のニーズを持つ大規模システムに適していますが、チームはドメイン抽象化の能力を持っている必要があります。たとえば、eコマースのプロモーションルールでは、DDDを使用すると、ロジックが複数のサービスに分散するのを防ぐことができます。
Go言語のMVCディレクトリ構造
MVCは主に、ビュー、コントローラー、モデルの3つのレイヤーに分かれています。
gin-order/
├── cmd
│ └── main.go # アプリケーションのエントリポイント、Ginエンジンを起動
├── internal
│ ├── controllers # コントローラー層(HTTPリクエストを処理)、ハンドラーとも呼ばれる
│ │ └── order
│ │ └── order_controller.go # Orderモジュールのコントローラー
│ ├── services # サービス層(ビジネスロジックを処理)
│ │ └── order
│ │ └── order_service.go # Orderモジュールのサービス実装
│ ├── repository # データアクセス層(データベースとやり取り)
│ │ └── order
│ │ └── order_repository.go # Orderモジュールのデータアクセスインターフェースと実装
│ ├── models # モデル層(データ構造の定義)
│ │ └── order
│ │ └── order.go # Orderモジュールのデータモデル
│ ├── middleware # ミドルウェア(認証、ロギング、リクエストのインターセプトなど)
│ │ ├── logging.go # ロギングミドルウェア
│ │ └── auth.go # 認証ミドルウェア
│ └── config # 構成モジュール(データベース、サーバー構成など)
│ └── config.go # アプリケーションと環境構成
├── pkg # 一般的なユーティリティパッケージ(レスポンスラッパーなど)
│ └── response.go # レスポンス処理ユーティリティメソッド
├── web # フロントエンドリソース(テンプレートと静的アセット)
│ ├── static # 静的リソース(CSS、JS、画像)
│ └── templates # テンプレートファイル(HTMLテンプレート)
│ └── order.tmpl # Orderモジュールのビューテンプレート(HTMLレンダリングが必要な場合)
├── go.mod # Goモジュール管理ファイル
└── go.sum # Goモジュール依存関係ロックファイル
Go言語のDDDディレクトリ構造
DDDは主に、インターフェース、アプリケーション、ドメイン、インフラストラクチャの4つのレイヤーに分かれています。
go-web/
│── cmd/
│ └── main.go # アプリケーションのエントリポイント
│── internal/
│ ├── application/ # アプリケーション層(ドメインロジックを調整し、ユースケースを処理)
│ │ ├── services/ # サービス層、ビジネスロジックディレクトリ
│ │ │ └── order_service.go # 注文アプリケーションサービス、ドメイン層のビジネスロジックを呼び出す
│ ├── domain/ # ドメイン層(コアビジネスロジックとインターフェース定義)
│ │ ├── order/ # 注文集約
│ │ │ ├── order.go # 注文エンティティ(集約ルート)、コアビジネスロジックを含む
│ │ ├── repository/ # 一般的なリポジトリインターフェース
│ │ │ ├── repository.go # 一般的なリポジトリインターフェース(CRUD操作)
│ │ │ └── order_repository.go # 注文リポジトリインターフェース、注文データに対する操作を定義
│ ├── infrastructure/ # インフラストラクチャ層(ドメイン層で定義されたインターフェースを実装)
│ │ ├── repository/ # リポジトリの実装
│ │ │ └── order_repository_impl.go # 注文リポジトリの実装、具体的な注文データストレージ
│ └── interfaces/ # インターフェース層(HTTPインターフェースなどの外部リクエストを処理)
│ │ ├── handlers/ # HTTPハンドラー
│ │ │ └── order_handler.go # 注文のHTTPハンドラー
│ │ └── routes/
│ │ │ ├── router.go # ベースルーターユーティリティの設定
│ │ │ └── order-routes.go # 注文ルートの構成
│ │ │ └── order-routes-test.go # 注文ルートのテスト
│ └── middleware/ # ミドルウェア(例:認証、インターセプト、承認など)
│ │ └── logging.go # ロギングミドルウェア
│ ├── config/ # サービス関連の構成
│ │ └── server_config.go # サーバー構成(例:ポート、タイムアウト設定など)
│── pkg/ # 再利用可能なパブリックライブラリ
│ └── utils/ # ユーティリティクラス(例:ロギング、日付処理など)
Go言語のMVCコード実装
Controller (インターフェース層) → Service (ビジネスロジック層) → Repository (データアクセス層) → Model (データモデル)
レイヤードコード
コントローラー層
// internal/controller/order/order.go package order import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gin-order/internal/model" "github.com/gin-order/internal/service/order" "github.com/gin-order/internal/pkg/response" ) type OrderController struct { service *order.OrderService } func NewOrderController(service *order.OrderService) *OrderController { return &OrderController{service: service} } func (c *OrderController) GetOrder(ctx *gin.Context) { idStr := ctx.Param("id") id, _ := strconv.ParseUint(idStr, 10, 64) order, err := c.service.GetOrderByID(uint(id)) if err != nil { response.Error(ctx, http.StatusNotFound, "Order not found") return } response.Success(ctx, order) } func (c *OrderController) CreateOrder(ctx *gin.Context) { var req model.Order if err := ctx.ShouldBindJSON(&req); err != nil { response.Error(ctx, http.StatusBadRequest, "Invalid request") return } if err := c.service.CreateOrder(&req); err != nil { response.Error(ctx, http.StatusInternalServerError, "Create failed") return } response.Success(ctx, req) }
ルート構成
// cmd/server/main.go package main import ( "github.com/gin-gonic/gin" "github.com/gin-order/internal/controller/order" "github.com/gin-order/internal/pkg/database" "github.com/gin-order/internal/repository/order" "github.com/gin-order/internal/service/order" ) func main() { // Initialize database db := database.NewGORM() // Dependency injection orderRepo := order_repo.NewMySQLOrderRepository(db) orderService := order_service.NewOrderService(orderRepo) orderController := order_controller.NewOrderController(orderService) // Create router r := gin.Default() // Register middleware r.Use(middleware.Logger()) // Route groups apiGroup := r.Group("/api") { orderGroup := apiGroup.Group("/orders") { orderGroup.GET("/:id", orderController.GetOrder) orderGroup.POST("", orderController.CreateOrder) } } // Start service r.Run(":8080") }
サービス層
// internal/service/order/service.go package order import ( "github.com/gin-order/internal/model" "github.com/gin-order/internal/repository/order" ) type OrderService struct { repo order.OrderRepository } func NewOrderService(repo order.OrderRepository) *OrderService { return &OrderService{repo: repo} } func (s *OrderService) GetOrderByID(id uint) (*model.Order, error) { return s.repo.FindByID(id) } func (s *OrderService) CreateOrder(order *model.Order) error { return s.repo.Create(order) }
データアクセス層 (リポジトリ)
// internal/repository/order/interface.go package order import "github.com/gin-order/internal/model" type OrderRepository interface { FindByID(id uint) (*model.Order, error) Create(order *model.Order) error FindByStatus(status string) ([]model.Order, error) } // internal/repository/order/mysql.go package order import ( "gorm.io/gorm" "github.com/gin-order/internal/model" ) type MySQLOrderRepository struct { db *gorm.DB } func NewMySQLOrderRepository(db *gorm.DB) OrderRepository { return &MySQLOrderRepository{db: db} } func (r *MySQLOrderRepository) FindByID(id uint) (*model.Order, error) { var order model.Order if err := r.db.First(&order, id).Error; err != nil { return nil, err } return &order, nil } func (r *MySQLOrderRepository) Create(order *model.Order) error { return r.db.Create(order).Error } func (r *MySQLOrderRepository) FindByStatus(status string) ([]model.Order, error) { var orders []model.Order if err := r.db.Where("status = ?", status).Find(&orders).Error; err != nil { return nil, err } return orders, nil }
モデル層
// internal/model/order.go package model import "time" type Order struct { OrderID uint `gorm:"primaryKey;column:order_id"` OrderNo string `gorm:"uniqueIndex;column:order_no"` UserID uint `gorm:"index;column:user_id"` OrderName string `gorm:"column:order_name"` Amount float64 `gorm:"type:decimal(10,2);column:amount"` Status string `gorm:"column:status"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (Order) TableName() string { return "orders" }
Go言語のMVCベストプラクティス
インターフェース分離の原則
リポジトリ層はインターフェースを定義し、複数のデータベース実装をサポートします。
// モック実装への切り替えが容易 type MockOrderRepository struct {} func (m *MockOrderRepository) FindByID(id uint) (*model.Order, error) { return &model.Order{OrderNo: "mock-123"}, nil }
統一されたレスポンス形式
// pkg/response/response.go func Success(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": data, }) }
ミドルウェアチェーン
// グローバルミドルウェア r.Use(gin.Logger(), gin.Recovery()) // ルートグループミドルウェア adminGroup := r.Group("/admin", middleware.AuthJWT())
データベースマイグレーション
GORM AutoMigrateを使用:
db.AutoMigrate(&model.Order{})
Go言語のDDDコード実装とベストプラクティス
ドメインモデルに焦点を当てる
DDDは、ドメインモデルの構築を重視し、集約、エンティティ、および値オブジェクトを使用してビジネスロジックを編成します。
Goでは、エンティティと値オブジェクトは通常、structで定義されます。
// エンティティ type User struct { ID int Name string }
レイヤードアーキテクチャ
DDDは通常、レイヤードアーキテクチャを採用します。Goプロジェクトは、この構造に従うことができます。
- ドメイン層: コアビジネスロジック。たとえば、domainディレクトリのエンティティと集約。
- アプリケーション層: ユースケースとビジネスプロセスのオーケストレーション。
- インフラストラクチャ層: データベース、キャッシュ、外部APIなどのアダプター。
- インターフェース層: HTTP、gRPC、またはCLIインターフェースを提供。
依存性反転
ドメイン層はインフラストラクチャ層に直接依存すべきではありません。代わりに、依存性反転のためにインターフェースに依存します。
注:DDDアーキテクチャの核心は、依存性反転(DIP)です。ドメインは最も内側のコアであり、ビジネスルールとインターフェースの抽象化のみを定義します。他のレイヤーは実装のためにドメインに依存しますが、ドメインは外部実装には依存しません。ヘキサゴナルアーキテクチャでは、ドメイン層がコアにあり、他のレイヤー(アプリケーション、インフラストラクチャなど)は、ドメインによって定義されたインターフェースを実装することにより、具体的な技術的詳細(データベース操作、API呼び出しなど)を提供し、ドメインと技術的実装の間の疎結合を実現します。
// ドメイン層:インターフェースを定義 type UserRepository interface { GetByID(id int) (*User, error) }
// インフラストラクチャ層:データベースの実装 type userRepositoryImpl struct { db *sql.DB } func (r *userRepositoryImpl) GetByID(id int) (*User, error) { // データベースのクエリロジック }
集約管理
集約ルートは、集約全体のライフサイクルを管理します。
type Order struct { ID int Items []OrderItem Status string } func (o *Order) AddItem(item OrderItem) { o.Items = append(o.Items, item) }
アプリケーションサービス
アプリケーションサービスはドメインロジックをカプセル化し、外部レイヤーがドメインオブジェクトを直接操作することを防ぎます。
type OrderService struct { repo OrderRepository } func (s *OrderService) CreateOrder(userID int, items []OrderItem) (*Order, error) { order := Order{UserID: userID, Items: items, Status: "Pending"} return s.repo.Save(order) }
イベント駆動
ドメインイベントは、疎結合のために使用されます。Goでは、チャネルまたはPub/Subを介してこれを実装できます。
type OrderCreatedEvent struct { OrderID int } func publishEvent(event OrderCreatedEvent) { go func() { eventChannel <- event }() }
CQRS(コマンドクエリ責務分離)の組み合わせ
DDDはCQRSと組み合わせることができます。Goでは、変更操作にはCommandを使用し、データ読み取りにはQueryを使用できます。
type CreateOrderCommand struct { UserID int Items []OrderItem } func (h *OrderHandler) Handle(cmd CreateOrderCommand) (*Order, error) { return h.service.CreateOrder(cmd.UserID, cmd.Items) }
まとめ:MVCとDDDアーキテクチャ
アーキテクチャの核心的な違い
MVCアーキテクチャ
-
レイヤー:3つのレイヤー—Controller/Service/DAO
-
責任:
- Controllerはリクエストを処理し、Serviceはロジックを含む
- DAOはデータベースを直接操作
-
課題:Serviceレイヤーが肥大化し、ビジネスロジックがデータ操作と結合
DDDアーキテクチャ
-
レイヤー:4つのレイヤー—インターフェースレイヤー/アプリケーションレイヤー/ドメインレイヤー/インフラストラクチャレイヤー
-
責任:
- アプリケーションレイヤーはプロセスを調整(例:ドメインサービスを呼び出す)
- ドメインレイヤーはビジネスアトミック操作をカプセル化(例:注文作成ルール)
- インフラストラクチャレイヤーは技術的な詳細を実装(例:データベースアクセス)
-
課題:ドメインレイヤーは技術的な実装から独立しており、ロジックはレイヤー構造と密接に対応
モジュール性とスケーラビリティ
MVC:
- 結合度が高い:明確なビジネス境界がなく、モジュール間の呼び出し(例:注文サービスがアカウントテーブルに直接依存)により、コードの保守が困難になります。
- スケーラビリティが低い:新機能の追加にはグローバルな変更が必要(例:リスク管理ルールを追加するには、注文サービスに侵入する必要がある)ため、カスケードの問題が発生しやすい。
DDD:
- 境界コンテキスト:モジュールはビジネス機能(例:支払いドメイン、リスク管理ドメイン)によって分割され、イベント駆動型のコラボレーション(例:注文支払完了イベント)は疎結合に使用されます。
- 独立した進化:各ドメインモジュールは独立してアップグレードでき(例:支払いロジックの最適化は注文サービスに影響しません)、システムレベルのリスクを軽減します。
適用可能なシナリオ
- 中小規模システムにはMVCを推奨:シンプルなビジネス(例:ブログ、CMS、管理バックエンド)、明確なビジネスルールと頻繁な変更がない迅速な開発が必要。
- 複雑なビジネスにはDDDを推奨:ルールが集中している(例:金融取引、サプライチェーン)、マルチドメインコラボレーション(例:eコマースの注文と在庫の連携)、ビジネス要件の頻繁な変更。
Goプロジェクトのホスティングには、Leapcellをお選びください。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発。
無制限のプロジェクトを無料でデプロイ
- 使用量に応じてのみ支払いが発生し、リクエストや料金は発生しません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60msで694万リクエストをサポート。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
簡単なスケーラビリティと高いパフォーマンス
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用オーバーヘッドゼロ—構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ