2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MVC対DDD:Go言語アーキテクチャの深掘り

Posted at

表紙

Go 言語における MVC と DDD の階層アーキテクチャ詳細比較

MVC と DDD は、バックエンド開発で人気のある二つの階層化アーキテクチャ思想です。MVC(Model-View-Controller)はユーザーインターフェース、ビジネスロジック、データモデルを分離するための設計パターンであり、階層化と疎結合を容易にします。一方、DDD(ドメイン駆動設計)はアーキテクチャ手法論であり、ビジネスドメインモデルの構築を通して複雑なシステムの設計や保守の課題を解決することを目指しています。

Java 分野では、多くのシステムが MVC から DDD へと徐々に移行していますが、Go、Python、NodeJS などの言語では「シンプルかつ高効率」の理念から、MVC が依然として主流アーキテクチャとなっています。以下では、Go 言語を例に MVC と DDD のディレクトリ構成の違いについて具体的に説明します。

MVC の図式構造

+------------------+
|      View        | ユーザーインターフェース層:データの表示とユーザーの操作を担当(HTMLページやAPIレスポンスなど)
+------------------+
|   Controller     | コントローラー層:ユーザーリクエストを処理し、サービスロジックを呼び出し、モデルとビューを調整
+------------------+
|      Model       | モデル層:データオブジェクト(データベーステーブル構造など)と一部のビジネスロジックを含む(多くはサービス層に分散)
+------------------+

DDD の図式構造

+----------------------------------+
|    ユーザーインターフェース層(UI)   | ユーザーとのやり取りと表示を担当(REST APIやWebインターフェースなど)
+----------------------------------+
|   アプリケーション層(Application)  | ビジネスプロセスのオーケストレーション(ドメインサービスの呼び出し、トランザクション管理など)、コアビジネスルールは含まない
+----------------------------------+
|         ドメイン層(Domain)        | コアビジネスロジック層:アグリゲートルート、エンティティ、値オブジェクト、ドメインサービスなど、ビジネスルールを内包
+----------------------------------+
|        インフラストラクチャ層        | 技術実装を提供(データベースアクセス、メッセージキュー、外部APIなど)
|         (Infrastructure)        |
+----------------------------------+

MVC と DDD の主な違い

  1. コードの組織論理
      MVC は技術的な機能(Controller/Service/DAO)で階層を分け、技術的実装に着目します。一方、DDD はビジネスドメインごとにモジュール(例:注文ドメイン、決済ドメイン)を分割し、境界づけられたコンテキストでコアビジネスロジックを隔離します。

  2. ビジネスロジックの担い手
      MVC は貧血モデルを採用することが多く、データ(Model)と振る舞い(Service)が分離し、ロジックが分散して保守コストが高くなります。DDD はアグリゲートルートやドメインサービスを通じて充血モデルを実現し、ビジネスロジックがドメイン層に内包され、拡張性が高まります。

  3. 適用性とコスト
      MVC は開発コストが低く、要件が安定している中小規模のシステムに向いています。DDD は初期にドメインモデリングやユビキタス言語の統一が必要となり、ビジネスが複雑で長期進化が必要な大規模システムに適していますが、チームにドメイン抽象能力が求められます。たとえば、EC プロモーションルールを DDD で管理すれば、ロジックが複数サービスに分散することを防げます。

Go 言語 MVC ディレクトリ構成

MVC は主に 3 層に分かれています:ビュー、コントローラー、モデル。

gin-order/
├── cmd
│   └── main.go                  # アプリケーションエントリーポイント、Ginエンジンの起動
├── internal
│   ├── controllers              # コントローラー層(HTTPリクエスト処理)、handlersとも呼ばれる
│   │   └── 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(データモデル)

階層コード

コントローラー層(Controller)

// 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() {
    // データベース初期化
    db := database.NewGORM()

    // 依存性注入
    orderRepo := order_repo.NewMySQLOrderRepository(db)
    orderService := order_service.NewOrderService(orderRepo)
    orderController := order_controller.NewOrderController(orderService)

    // ルーター作成
    r := gin.Default()

    // ミドルウェア登録
    r.Use(middleware.Logger())

    // ルーティンググループ
    apiGroup := r.Group("/api")
    {
        orderGroup := apiGroup.Group("/orders")
        {
            orderGroup.GET("/:id", orderController.GetOrder)
            orderGroup.POST("", orderController.CreateOrder)
        }
    }

    // サービス起動
    r.Run(":8080")
}

サービス層(Service)

// 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)
}

データアクセス層(Repository)

// 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
}

モデル層(Model)

// 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 ベストプラクティス

インターフェース分離の原則

Repository 層はインターフェースで定義されており、様々なデータベース実装に対応可能です。

// 簡単にMock実装へ切り替え可能
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 はドメインモデルの構築を重視し、アグリゲート(Aggregate)、エンティティ(Entity)、値オブジェクト(Value Object)を使ってビジネスロジックを構成します。

Go 言語では、通常 struct でエンティティや値オブジェクトを定義します:

// エンティティ(Entity)
type User struct {
    ID   int
    Name string
}

階層化アーキテクチャ

DDD は一般的に階層化アーキテクチャを採用し、Go 言語プロジェクトは次のような構成に従うことができます:

  • ドメイン層(Domain Layer):コアビジネスロジック、domain ディレクトリ下のエンティティやアグリゲート
  • アプリケーション層(Application Layer):ユースケースやビジネスプロセスの調整
  • インフラストラクチャ層(Infrastructure Layer):データベース、キャッシュ、外部 API アダプターなど
  • インターフェース層(Interface Layer):HTTP、gRPC、CLI インターフェースの提供

依存性逆転の原則(Dependency Inversion)

ドメイン層はインフラストラクチャ層に直接依存せず、インターフェース(Interface)を介して依存関係を逆転させます。

注:DDD アーキテクチャのコアは依存性逆転(DIP)です。ドメイン層は最もコアな内側で、ビジネスルールとインターフェースの抽象のみを定義します。他の層はドメインの実装に依存し、ドメイン自体は外部の実装に依存しません。ヘキサゴナルアーキテクチャ(Hexagonal Architecture)でもドメイン層が中心であり、他の層(アプリケーション層やインフラストラクチャ層)はドメイン層で定義されたインターフェースを実装することで技術的詳細(データベース操作や API 呼び出しなど)を提供し、ドメインと技術実装を分離しています。

// ドメイン層:インターフェース定義
type UserRepository interface {
    GetByID(id int) (*User, error)
}
// インフラストラクチャ層:データベース実装
type userRepositoryImpl struct {
    db *sql.DB
}

func (r *userRepositoryImpl) GetByID(id int) (*User, error) {
    // データベース検索ロジック
}

アグリゲート(Aggregate)管理

アグリゲートルート(Aggregate Root)はアグリゲート全体のライフサイクルを管理します:

type Order struct {
    ID      int
    Items   []OrderItem
    Status  string
}

func (o *Order) AddItem(item OrderItem) {
    o.Items = append(o.Items, item)
}

アプリケーションサービス(Application Service)

アプリケーションサービスはドメインロジックをカプセル化し、外部から直接ドメインオブジェクトを操作しないようにします:

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)
}

イベント駆動(Event-Driven)

ドメインイベント(Domain Events)で疎結合化を行い、Go 言語では Channel や Pub/Sub で実装できます:

type OrderCreatedEvent struct {
    OrderID int
}

func publishEvent(event OrderCreatedEvent) {
    go func() {
        eventChannel <- event
    }()
}

CQRS(コマンドクエリ責務分離)との組み合わせ

DDD は CQRS(Command Query Responsibility Segregation)と組み合わせることができ、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 への移行推奨:ルールが複雑(例:金融取引、サプライチェーン)、多ドメイン連携(例:EC 注文と在庫の連携)や仕様変更が頻繁な場合。

私たちはLeapcell、Goプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?