Go言語で学ぶ Web アプリケーション開発4：[デプロイ & DB負荷対策]

## はじめに

前回の記事では、Dockerを使ったコンテナ化とgRPCを導入し、よりスケーラブルなWebアプリケーションを構築しました。  
今回は、簡易的なHerokuへのデプロイ方法と、大量のToDoデータを扱う際のパフォーマンス最適化（負荷対策）について解説します。

**対象読者**
- Goで開発したWebアプリをデプロイしたい方
- Webアプリのパフォーマンスを改善し、大量データに対応できる設計を学びたい方

**目次**
1. Heroku へのデプロイ
   - Herokuのセットアップ
   - HerokuにGoアプリをデプロイする手順
2. ToDoアプリの負荷対策
   - インデックス最適化（データベース）
   - キャッシュ戦略（Redis の活用）
   - 非同期処理（Go の Goroutine を活用）
   - ページネーション（大量データの取得を最適化）

---

## 1. Herokuへのデプロイ

**1.1 Herokuのセットアップ**

まず、Heroku CLIをインストールして、Herokuアカウントにログインします。

```sh
# Heroku CLI をインストール（Mac）
brew install heroku

# ログイン
heroku login
```

**1.2 HerokuにGoアプリをデプロイする手順**

1. **Heroku用のProcfileを作成**
   Herokuは`Procfile`を使ってアプリの起動方法を定義します。

   **Procfile**
   ```sh
   web: ./main
   ```

2. **Heroku用の`go.mod`の作成**
   ```sh
   go mod init example.com/myapp
   go mod tidy
   ```

3. **Herokuにアプリをプッシュ & デプロイ**
   ```sh
   git init
   git add .
   git commit -m "Initial commit"
   heroku create my-go-todo-app
   git push heroku main
   ```

4. **Herokuでアプリを実行 & 確認**
   ```sh
   heroku open
   ```

---

## 2. ToDo アプリの負荷対策

**2.1 インデックス最適化（データベース）**

- ToDoの数が増えてくると、データベースの検索速度が遅くなる。
- インデックスを設定することで、検索を高速化できる。

```go
import "gorm.io/gorm"

type Todo struct {
    ID     uint   `gorm:"primaryKey"`
    Task   string `json:"task" gorm:"index"`
    Status bool   `json:"status"`
}
```

- `gorm:"index"`をつけることで、データベース側で`Task`フィールドの検索が最適化される。

---

**2.2 キャッシュ戦略（Redisの活用）**

- 頻繁に取得するデータを**Redis**にキャッシュすることで、DB負荷を軽減する。
- `github.com/go-redis/redis/v8` を利用。

#### **Redisの導入**
```sh
go get github.com/go-redis/redis/v8
```

#### **Redisクライアントのセットアップ**
```go
import (
    "context"
    "github.com/go-redis/redis/v8"
)

var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})
```

#### **キャッシュを使った ToDo 取得**
```go
func GetTodos(c *gin.Context) {
    cached, err := rdb.Get(ctx, "todos").Result()
    if err == nil {
        c.JSON(http.StatusOK, cached)
        return
    }
    
    var todos []models.Todo
    database.DB.Find(&todos)
    rdb.Set(ctx, "todos", todos, time.Minute*10)
    c.JSON(http.StatusOK, todos)
}
```
- `Redis`にデータがあればキャッシュから返し、なければDBから取得してキャッシュに保存。

---

### **2.3 非同期処理（Goroutineの活用）**

- **新しいToDoの登録時に、非同期でログを記録する。**

```go
func CreateTodo(c *gin.Context) {
    var newTodo models.Todo
    if err := c.ShouldBindJSON(&newTodo); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    database.DB.Create(&newTodo)
    
    go func(task string) {
        log.Printf("New ToDo created: %s", task)
    }(newTodo.Task)
    
    c.JSON(http.StatusCreated, newTodo)
}
```

- `go func()`を使うことで、**非同期でログの記録**を行い、メインスレッドのレスポンス速度を落とさないようにできる。

---

### **2.4 ページネーション（大量データの取得を最適化）**

- **ページネーション**を導入し、1回のAPIリクエストで取得するデータ量を制限。

```go
func GetTodosPaginated(c *gin.Context) {
    var todos []models.Todo
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    limit := 10
    offset := (page - 1) * limit
    database.DB.Limit(limit).Offset(offset).Find(&todos)
    c.JSON(http.StatusOK, todos)
}
```

- `GET /api/todos?page=2`のようにページ番号を指定すると、**10件ずつデータを取得**するようになる。

---

## まとめ

| 項目 | 説明 |
|------|------|
| **Heroku デプロイ** | Heroku CLIを使って簡単にGoアプリをデプロイ |
| **インデックス最適化** | データベース検索を高速化し、レスポンス遅延を軽減 |
| **Redis キャッシュ** | 頻繁にアクセスするデータをキャッシュし、DB負荷を削減 |
| **非同期処理 (Goroutine)** | ToDoの作成時にログ記録を非同期で実行し、API応答速度を向上 |
| **ページネーション** | 一度に大量のデータを取得せず、適切なデータ量を取得 |

本記事では、簡単なデプロイ方法と、大量のToDoデータを扱う際の負荷対策について解説しました。  
次は、Kubernetesを活用したデプロイ方法とか、負荷テストの実施について補足がてらにまとめようかな...