Goプロジェクトのアーキテクチャ

Goには、フレームワークによって強制されるプロジェクト構造はありません。しかし、それはつまり、自分で設計しなければならないということです。よく考え抜かれたアーキテクチャがあれば、プロジェクトが1,000行になっても構造が明確であり続け、100,000行に達しても管理しやすさを維持できます。

コードがすべてmain.goに詰め込まれているプロジェクトを引き継いだ場合、リファクタリングの最初のステップは、そのコードを適切なディレクトリに移動することです。

1. 学習内容


2. バックエンドエンジニアの実話

(1) 課題:10万行のコードがすべて main.go に含まれている

アリスはあるEコマースプロジェクトを引き受けた:

「前任者は、ルート登録、データベース操作、HTMLテンプレート、ビジネスロジックといったすべてのコードをmain.goに詰め込んでいました。3万行ものコードがごちゃ混ぜになっていたのです。ユーザーステータスのフィールドを追加しようとしたとき、正しく動作させるために5カ所も変更を加える必要がありました。機能1つを追加するのに2週間かかり、コードを探すだけで3日も費やしてしまいました。」

GO
// 悪いコード:すべてを main.go に詰め込む
package main

var db *sql.DB

func main() {
    // データベース接続
    // ルーティング登録
    // HTML テンプレート
    // ユーザー処理関数
    // 注文処理関数
    // 商品処理関数
    // すべて混在!
}

// 「ユーザー」関連コードを探す:Ctrl+F で "user" を検索、50 箇所に散らばっている

(2) Goのソリューション:4層アーキテクチャ

TEXT
myapp/
├── cmd/
│   └── server/
│       └── main.go         # エントリポイント → 依存性注入 + サービス起動
├── internal/
│   ├── handler/            # 層 1:HTTP ハンドラ(リクエストの解析/レスポンスを返す)
│   ├── service/            # 層 2:ビジネスロジック(ドメインルール)
│   └── repository/         # 層 3:データアクセス(データベース/外部 API)
├── pkg/
│   └── model/              # 層 4:ドメインモデル(データ構造)
└── go.mod

(3) 回帰:カオス対層化

シナリオ カオス型アーキテクチャ 4層アーキテクチャ
新規フィールド 場所不明の変更が5件 モデルとハンドラーのみの変更
データベースの切り替え すべてのデータベース呼び出しを更新 リポジトリ層のみを更新
ユニットテスト ユニットテストが実行できない モックを使用すれば各レイヤーを個別にテストできる
初心者向け入門 30,000行にわたるmain.goをざっと見てみる ディレクトリ名を見るだけで役割を理解する

3. 標準的なプロジェクト構成

(1) ディレクトリ構造の詳細な説明

TEXT
myproject/
├── cmd/                    # 実行ファイルのエントリポイント
│   ├── server/             #   server バイナリ
│   │   └── main.go
│   └── migrate/            #   データベースマイグレーションツール
│       └── main.go
├── internal/               # プライベートパッケージ(外部からのインポート不可)
│   ├── handler/            #   HTTP プロセッサ
│   ├── service/            #   ビジネスロジック
│   └── repository/         #   データへのアクセス
├── pkg/                    # エクスポート可能な共有パッケージ
│   └── model/              #   ドメインモデル
├── migrations/             # SQL マイグレーションファイル
├── config/                 # プロファイル
├── scripts/                # 補助スクリプト
├── go.mod
└── go.sum

(2) cmd/ / internal/ / pkg/ の担当範囲

目次 用途 外部からのインポートが可能
cmd/ 実行可能エントリポイント(main関数) 該当なし(メインパッケージであるため)
internal/ プライベート実装 ❌ Goコンパイラでは外部からのインポートが禁止されています
pkg/ 外部に公開されているコード
💡 ヒント: internal ディレクトリは Go コンパイラ専用のディレクトリであり、internal の親ディレクトリ外にあるパッケージからはこれをインポートすることはできません。これにより真のカプセル化が実現され、「命名規則」(_private など)よりも安全です。


4. クリーンアーキテクチャの4層モデル

TEXT
handler (HTTP) → service (事業) → repository (データ)
     ↓                ↓                ↓
   リクエストの解析         ドメインルール         データベース/API
   応答の返却         業務管理         CRUD 操作
   パラメータの検証         多段階のオーケストレーション        キャッシュへのアクセス

▶ サンプル:4層構成の実装

GO
// ---------- 層 4:Model(ドメインモデル) ----------
// pkg/model/ユーザー.go
パッケージ model

type User struct {
    ID        int
    Name      文字列
    Email     文字列
    CreatedAt time.Time
}

type CreateUserRequest struct {
    Name  文字列
    Email 文字列
}

// ---------- 層 3:Repository(データアクセス) ----------
// internal/リポジトリ/ユーザー.go
パッケージ リポジトリ

import (
    "データベース/sql"
    "myapp/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, エラー)
    Create(req model.CreateUserRequest) (*model.User, エラー)
    List() ([]*model.User, エラー)
    Delete(id int) エラー
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, エラー) {
    row := r.db.QueryRow("SELECT id, name, email, created_at FROM users WHERE id = ?", id)
    ユーザー := &model.User{}
    err := row.Scan(&ユーザー.ID, &ユーザー.Name, &ユーザー.Email, &ユーザー.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return ユーザー, err
}

func (r *userRepository) Create(req model.CreateUserRequest) (*model.User, エラー) {
    result, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", req.Name, req.Email)
    if err != nil {
        return nil, err
    }
    id, _ := result.LastInsertId()
    return r.FindByID(int(id))
}

// ---------- 層 2:Service(ビジネスロジック) ----------
// internal/service/ユーザー.go
パッケージ service

import (
    "errors"
    "myapp/internal/リポジトリ"
    "myapp/pkg/model"
    "strings"
)

var (
    ErrUserNotFound    = errors.New("ユーザー not found")
    ErrInvalidName     = errors.New("name is required")
    ErrInvalidEmail    = errors.New("invalid email format")
)

type UserService struct {
    repo リポジトリ.UserRepository
}

func NewUserService(repo リポジトリ.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Create(req model.CreateUserRequest) (*model.User, エラー) {
    if strings.TrimSpace(req.Name) == "" {
        return nil, ErrInvalidName
    }
    if !strings.Contains(req.Email, "@") {
        return nil, ErrInvalidEmail
    }
    return s.repo.Create(req)
}

func (s *UserService) Get(id int) (*model.User, エラー) {
    ユーザー, err := s.repo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if ユーザー == nil {
        return nil, ErrUserNotFound
    }
    return ユーザー, nil
}

// ---------- 層 1:Handler(HTTP ハンドラ) ----------
// internal/handler/ユーザー.go
パッケージ handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"

    "myapp/internal/service"
    "myapp/pkg/model"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req model.CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    ユーザー, err := h.svc.Create(req)
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, ユーザー)
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(r.PathValue("id"))

    ユーザー, err := h.svc.Get(id)
    if errors.Is(err, service.ErrUserNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal エラー")
        return
    }

    writeJSON(w, http.StatusOK, ユーザー)
}

// ---------- ユーティリティ関数 ----------

func writeJSON(w http.ResponseWriter, ステータス int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(ステータス)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, ステータス int, msg 文字列) {
    writeJSON(w, ステータス, map[文字列]文字列{"エラー": msg})
}
▶ 試してみよう

(1) 依存関係の方向

TEXT
Handler → Service → Repository (→ DB)
   |          |           |
   ↓          ↓           ↓
  Model     Model       Model
レイヤー 役割 依存関係 テスト可能
ハンドラー HTTPの解析・応答 サービス モックサービス
サービス ビジネスルール/オーケストレーション リポジトリ モックリポジトリ
リポジトリ データのCRUD データベース/SQL 模擬データベース / 統合テスト
モデル データ構造 なし
🔥 よくある間違い: 依存関係は、外側の層から内側の層へと流れる必要があります。 ハンドラーはサービスを認識し、サービスはリポジトリを認識しますが、リポジトリはサービスを認識してはなりません。各層は、その直下の層にのみ依存し、インターフェースを通じて結合を解除します(Goの暗黙的なインターフェースにより、これが自然に感じられます)。


5. 依存性注入

▶ サンプル:コンストラクタ注入

GO
// cmd/server/main.go
package main

import (
    "database/sql"
    "log"
    "net/http"

    "myapp/internal/handler"
    "myapp/internal/repository"
    "myapp/internal/service"
)

func main() {
    // ---- 依存性注入(全レイヤーを組み立てる) ----

    // 1. データベース接続
    db, err := sql.Open("sqlite3", "./app.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 2. Repository 層
    userRepo := repository.NewUserRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    // 3. Service 層(Repository に依存)
    userSvc := service.NewUserService(userRepo)
    orderSvc := service.NewOrderService(orderRepo, userRepo)

    // 4. Handler 層(Service に依存)
    userHandler := handler.NewUserHandler(userSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    // 5. ルーティング登録
    mux := http.NewServeMux()
    userHandler.Register(mux, "/api/v1/users")
    orderHandler.Register(mux, "/api/v1/orders")

    log.Println("サービスの開始は :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
▶ 試してみよう

(2) 依存性注入手法の比較

手法 実装 利点 欠点
コンストラクタ注入 NewXxx(dep) 明示的、コンパイル時チェック 依存関係によりコードが長くなる
手動での実装 メイン関数の組み立て サードパーティ製ライブラリは不要 大規模なプロジェクトではメンテナンスが煩雑
Google Wire コード生成 自動挿入 学習曲線
💡 ヒント: 小規模なプロジェクトであれば、手動での依存性注入で十分です。プロジェクトに20以上のサービスと50以上の依存関係がある場合は、Google Wire (github.com/google/wire) を使用して、依存性注入コードを自動生成することを検討してください。


6. 完全な例:Eコマース・プロジェクトのフレームワーク

GO
// cmd/サーバー/main.go
パッケージ main

import (
    "context"
    "データベース/sql"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// ---------- Model (pkg/model) ----------

type Product struct {
    ID    int     `json:"id"`
    Name  文字列  `json:"name"`
    Price float64 `json:"price"`
}

type Order struct {
    ID        int       `json:"id"`
    UserID    int       `json:"user_id"`
    ProductID int       `json:"product_id"`
    Quantity  int       `json:"quantity"`
    Total     float64   `json:"total"`
    Status    文字列    `json:"ステータス"`
    CreatedAt time.Time `json:"created_at"`
}

// ---------- Repository (internal/リポジトリ) ----------

type ProductRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

func (r *ProductRepository) FindByID(id int) (*Product, エラー) {
    p := &Product{}
    err := r.db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Price)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *ProductRepository) List() ([]*Product, エラー) {
    rows, err := r.db.Query("SELECT id, name, price FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*Product
    for rows.Next() {
        p := &Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

type OrderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) *OrderRepository {
    return &OrderRepository{db: db}
}

func (r *OrderRepository) Create(順序 *Order) エラー {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total, ステータス) VALUES (?, ?, ?, ?, ?)",
        順序.UserID, 順序.ProductID, 順序.Quantity, 順序.Total, 順序.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    順序.ID = int(id)
    return nil
}

func (r *OrderRepository) FindByID(id int) (*Order, エラー) {
    o := &Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total, ステータス, created_at FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.Total, &o.Status, &o.CreatedAt)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

// ---------- Service (internal/service) ----------

type OrderService struct {
    productRepo *ProductRepository
    orderRepo   *OrderRepository
}

func NewOrderService(productRepo *ProductRepository, orderRepo *OrderRepository) *OrderService {
    return &OrderService{
        productRepo: productRepo,
        orderRepo:   orderRepo,
    }
}

type PlaceOrderInput struct {
    UserID    int
    ProductID int
    Quantity  int
}

var (
    ErrProductNotFound = &AppError{Code: "PRODUCT_NOT_FOUND", Message: "その商品は存在しません", HTTPStatus: 404}
    ErrInsufficientStock = &AppError{Code: "INSUFFICIENT_STOCK", Message: "在庫不足", HTTPStatus: 409}
)

type AppError struct {
    Code       文字列 `json:"code"`
    Message    文字列 `json:"message"`
    HTTPStatus int    `json:"-"`
}

func (e *AppError) Error() 文字列 {
    return e.Message
}

func (s *OrderService) PlaceOrder(input PlaceOrderInput) (*Order, エラー) {
    product, err := s.productRepo.FindByID(input.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }

    total := product.Price * float64(input.Quantity)

    順序 := &Order{
        UserID:    input.UserID,
        ProductID: input.ProductID,
        Quantity:  input.Quantity,
        Total:     total,
        Status:    "created",
    }

    if err := s.orderRepo.Create(順序); err != nil {
        return nil, err
    }

    return 順序, nil
}

// ---------- Handler (internal/handler) ----------

type OrderHandler struct {
    svc *OrderService
}

func NewOrderHandler(svc *OrderService) *OrderHandler {
    return &OrderHandler{svc: svc}
}

func (h *OrderHandler) Register(mux *http.ServeMux, basePath 文字列) {
    mux.HandleFunc("POST "+basePath, h.PlaceOrder)
    mux.HandleFunc("GET "+basePath+"/{id}", h.GetOrder)
}

type placeOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

func (h *OrderHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
    // Context からユーザー ID を取得(認証ミドルウェアが注入)
    userID := r.Context().Value("user_id").(int)

    var req placeOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    順序, err := h.svc.PlaceOrder(PlaceOrderInput{
        UserID:    userID,
        ProductID: req.ProductID,
        Quantity:  req.Quantity,
    })

    if appErr, ok := err.(*AppError); ok {
        writeError(w, appErr.HTTPStatus, appErr.Message)
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "internal エラー")
        return
    }

    writeJSON(w, http.StatusCreated, 順序)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    // ... 実装
}

func writeJSON(w http.ResponseWriter, ステータス int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(ステータス)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, ステータス int, msg 文字列) {
    writeJSON(w, ステータス, map[文字列]文字列{"エラー": msg})
}

// ---------- Main(依存性注入のエントリポイント) ----------

func main() {
    db, err := sql.Open("sqlite3", "./shop.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // テーブルの初期化
    db.Exec(`CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price REAL
    )`)
    db.Exec(`CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER,
        product_id INTEGER, quantity INTEGER, total REAL,
        ステータス TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`)

    // 依存性注入
    productRepo := NewProductRepository(db)
    orderRepo := NewOrderRepository(db)
    orderSvc := NewOrderService(productRepo, orderRepo)
    orderHandler := NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    orderHandler.Register(mux, "/api/v1/orders")

    // ヘルスチェック
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[文字列]文字列{"ステータス": "ok"})
    })

    サーバー := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // グレースフルシャットダウン
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        log.Println("サービスを停止中です...")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        サーバー.Shutdown(ctx)
    }()

    log.Println("Eコマースサービスは :8080")
    if err := サーバー.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}
100%
graph TB
    subgraph Handler
        H[HTTP Handler]
    end
    subgraph Service
        S[Business Logic]
    end
    subgraph Repository
        R[Data Access]
    end
    subgraph Model
        M[Domain Structs]
    end
    subgraph External ["外部依存関係"]
        DB[(Database)]
    end

    H -->|呼び出し| S
    S -->|呼び出し| R
    R -->|検索| DB
    H --- M
    S --- M
    R --- M

    style Handler fill:#e1f5fe
    style Service fill:#fff3e0
    style Repository fill:#e8f5e9
    style Model fill:#f3e5f5
🔥 よくある間違い: どのレイヤーも飛ばさないでください。 たとえサービスがリポジトリのメソッドを1つだけ呼び出すだけであっても、必ずサービス層を経由する必要があります。将来、ビジネスルールが拡張される可能性があるからです。レイヤーを飛ばすと、ビジネスロジックがハンドラー全体に散らばってしまい、単体テストができなくなってしまいます。


❓ よくある質問

Q クリーンアーキテクチャとは何ですか?
A 階層化されたアーキテクチャパターンです。外側から内側へと順に、ハンドラー(インターフェース層)→ サービス(ユースケース層)→ リポジトリ(データ層)→ モデル(ドメイン層)となります。基本原則:依存関係は外側の層から内側の層へと流れる。内側の層は外側の層の存在を認識しない。外側の層はインターフェースを実装し、内側の層はインターフェースを定義する。
Q 4層モデルにおける各層はどのように構成されていますか?
A (1) ハンドラー:HTTPの解析・レスポンス、パラメータの検証;(2) サービス:ビジネスルール、多段階のオーケストレーション、トランザクション;(3) リポジトリ:データベースのCRUD操作、キャッシュへのアクセス、外部API;(4) モデル:データ構造の定義。各層は、その直下の層のみに依存し、インターフェースを通じて疎結合になっています。
Q 依存性注入はどのように実装されますか?
A コンストラクタ注入が最も一般的なアプローチです:NewXxx(dep1, dep2) *Xxx。すべての依存関係は、main関数(またはWireによって生成されたコード)内で作成・組み立てられます。利点:(1) 依存関係の明確化、(2) コンパイル時のチェック、(3) ユニットテスト時にモックを渡すことができること。
Q なぜ internal ディレクトリを使うのですか?
A Go コンパイラは、internal ディレクトリ内のパッケージが、その親ディレクトリ内のコードからのみインポートされるように強制します。これにより、真のカプセル化が実現され、外部のユーザーは internal パッケージをインポートできなくなります。大規模なプロジェクトでは、これによりアーキテクチャ上の違反(例えば、handlersリポジトリに直接インポートされるようなケース)を防ぐことができます。
Q エラー型はどのように設計すべきですか?
A Code、Message、HTTPStatus フィールドを含むカスタムエラー型を定義します。サービス層はビジネスエラー(ErrProductNotFound など)を返し、ハンドラ層はこれらのエラーを HTTP ステータスコードおよび JSON レスポンスにマッピングします。メリット:統一されたエラー形式により、エラーの種類が一目でわかります。
Q レイヤー間でインターフェースを使用する必要はありますか?
A 推奨されます。Goの暗黙的なインターフェースにより、依存性の反転が自然に実現されます。つまり、リポジトリがインターフェースを定義し、ハンドラがそのインターフェースに依存し、具体的な実装はコンストラクタを介して注入されます。利点:(1) ユニットテストでモックを使用できる;(2) 実装の切り替え(例:SQLite → MySQL)を行う際、呼び出し側を変更する必要がない。
Q 小規模なプロジェクトでも4層アーキテクチャが必要ですか?
A いいえ。アーキテクチャの深さはプロジェクトの規模によって決まります。コード行数が500行未満のプロジェクトにはフラットな構造を、500~5,000行のプロジェクトには2層構造(ハンドラー+リポジトリ)を、5,000行以上のプロジェクトには4層構造を採用してください。まずは2層アーキテクチャから始め、プロジェクトの成長に合わせて段階的に層を追加していくことをお勧めします。過剰な設計は避けましょう。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):以下のディレクトリを含むプロジェクトの骨格を作成してください:cmd/サーバー/main.go、internal/handler/、internal/service/、internal/リポジトリ/、および pkg/model/。HealthHandler を返す単純な {"ステータス": "ok"} を実装してください。

  2. 上級問題(難易度:⭐⭐):4層アーキテクチャを用いて商品カテゴリ管理を実装してください。要件:(1) モデル:Category(id, name, parent_id); (2) ハンドラー:CRUDエンドポイント; (3) サービス:カテゴリ名が一意であることを検証し、循環参照を防止する; (4) リポジトリ:SQLiteを使用して実装; (5) main 関数は、コンストラクタ注入を介してコンポーネントを組み立てる。

  3. 課題(難易度:⭐⭐⭐):第22課のライブラリ管理システムを、4層アーキテクチャにリファクタリングしてください。要件:(1) コードを cmd/internal/pkg ディレクトリに移動すること;(2) ハンドラ層はHTTPのみを処理すること;(3) サービス層には完全なビジネスバリデーションを含めること; (4) リポジトリ層ではSQLiteを使用すること;(5) モックテストを可能にするため、リポジトリをインターフェースとして公開すること;(6) サービス層のビジネスルールをテストするためのユニットテスト(モックリポジトリを使用)を作成すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%