Progate(AWSハッカソン)でスクラム開発業務改善するアプリ作成に取り組んだが、めちゃくちゃ勉強になったので、振り返りがしたい。

# はじめに
今回2回目となるハッカソンの出場となりました。実際に出場してみて、自分が日頃勉強するよりも、何倍も勉強になって、めちゃくちゃ成長を実感しました。その軌跡を残していきたいと思い、記事を書きました。

# どういうハッカソンに参加したの？
https://progate.connpass.com/event/342402/

Progateが主催する2日間のハッカソンイベントで、なんとAWS様と協力して、無料のAWS環境を提供してくれるという神ハッカソンでした！ちょうどAWSについて勉強していて、その勉強したことを無料で、試せるというのは、ビックチャンスなので<b>「申し込むしかない！！」</b>と思い、申し込みました。元々面識あるチームで参加したので、事前に何を作るかを話し合いました。


# 作ったものは？
https://topaz.dev/projects/2652658a53e9ec6e1d72




メンバーが、長期インターンで、スクラム開発をしていて、その反省点や改善点をmiroというボードで書くという感じなのですが、何月何日にどの反省をしたのかが、わからない。さらに、なんの反省をしたのかを忘れる。これでは、反省した意味がない、同じ失敗を繰り返す。。ということで、、

- 何月何日の反省点や改善点はこれですと示してくれる機能 + さらにslackの通知機能でも反省点と改善点を示してくれる。
- さらにAIがその反省点や改善点をもとに次何をすべきかを教えてくれる。

これによって、スクラム開発をより効率的に行えるということで、この方針で開発が決まりました。

miroはこれ↓

https://miro.com/ja/


# 自分の担当は？
私は、バックエンド担当で、Go言語について、勉強していたので、せっかくなので、Go言語を使って、バックエンド処理を書いてみようと思いました。

### 自分が作った部分
1. miroapiを使って、ボードの付箋情報のテキスト情報を取得する。
2. その取得したテキスト情報を、(PostgreSQL)データベースに格納する処理。
3. データベースに格納する際のテーブル設計
4. AWSのlambda関数を使って、ここの処理を毎時間ごとに自動で行ってくれる機能作成。

これらを作成しました。4については、AWS上のデータベース、RDSに格納するということは、行えましたが、自動化するという機能は作ることができませんでした。

<b>※ Lambda から RDS に直接データを格納する方法は、データベースの負荷が高まり、パフォーマンスの低下や処理落ちのリスクがあるため、適切ではないとご指摘を受けました。
そのため、RDSではなく、DynamoDB を利用する方法を考えなければならないと思いました。</b>


# 工夫した点

今回ただ、Goの処理を適当に書いただけでなく、将来の拡張・保守性を考慮して、「クリーンアーキテクチャ手法」で設計を行いました。


### クリーンアーキテクチャって？
ソフトウェア設計の原則の1つで、依存関係を適切に管理し、テストしやすく、変更に強いシステムを構築することを目的としたアーキテクチャパターンのことです。

![b1041924432af01ffd983ba5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3825496/7bfc52eb-2e81-4306-a049-15d9481422c3.png)


- 基本構造
    - エンティティ（一番中心側）：システムの最も重要なビジネスルールをカプセル化する。
アプリケーションの種類を問わず再利用可能なモデル、変更されることがほぼない層
    - ユースケース：ビジネスルールを実行する、エンティティを操作し、具体的なアプリケーションの振る舞いを定義する部分です。
    - インターフェースアダプター：データの入出力を行う層で、ユースケース層と一番外側のフレームワーク、ドライバの橋渡しをしてくれる部分です。
    - フレームワーク、ドライバ （一番外側）：最も外側の層、フレームワークや外部ライブラリとの接続部分です。DB、Webフレームワーク、認証システムなど

 #### 重要なポイント
クリーンアーキテクチャで一番重要なことは、<b>「依存関係の方向を制御すること」</b>です。

① 依存関係逆転の原則：外側の層（詳細）→ 内側の層（ビジネスロジック）に依存しないようにする。この依存がないようにinterfaceを活用する。

② ビジネスロジック（エンティティとユースケース）：外部技術（DB,Web）に依存しないように！DBやフレームワークを変更しても、ユースケースのロジックは変えないで済むようにする。

③ テストしやすい構造：外部に依存しないため、ユースケースの単体テストが容易になる。

ここで、私は、最初つまづきました。。
- 依存関係の制御にinferfaceが絡んでくる...???
- そもそもinterfaceってなに？？？

次はここらへんを深掘りしたいと思います。

# interfaceについて
interfaceは、簡単に言えば、<b>「抽象化」</b>をおこなっています。
例えば、犬、猫、たぬき、うさぎを抽象化すると、「動物」ですよね？
MySQL、SQLite、PostgreSQLを抽象化すると、「データベース」になると思います。
### この抽象化というのが、とても重要です。

先ほど、基本構造で、インターフェースアダプターという層があったと思います。interfaceは、ここに位置しています。ここで、抽象化を行うことでいいことがあります。

### 抽象化することのメリット
クリーンアーキテクチャにおいて、interface（インターフェース）を使った抽象化は、
ユースケースが、「具体的な技術（データベース、Web API、外部ライブラリなど）に依存しない設計」を可能にします。

具体的には、
① データベースの変更が容易になる：例えば、最初は MySQL を使っていたが、後で PostgreSQL に変更することになった場合、interface を使っていれば、ユースケースのコードを変更せずに済みます。ユースケースは「データを取得する」という抽象的な処理だけを実装すればよく、どの DB から取得するかは意識しなくて済みます。

② ユースケースが影響を受けない（疎結合）：具体的な DB の処理が UserRepository インターフェース に隠蔽されるため、「データの取得・保存の処理」が変わっても、ユースケースには影響を受けなくなります。また、新しいデータストレージ（DynamoDB など）を追加する場合も、既存コードをほぼ変えずに対応可能になり、開発の効率が上がります。

# Goでクリーンアーキテクチャを取り入れた場合のディレクトリ構成
今回は、以下のようなディレクトリ構成にしました。

```
/backend
├── cmd/     # 最終的な実行部分を含む
│└── main.go  
│
├── config/    # アプリケーションの設定や外部サービスの接続設定 
│└── config.go 
│
├── controller/         
│└── controller.go 
│
├── db/        # データベースとの接続を実際にしたり、SQLの実行をする。
│└── database.go 
│
├── middleware/   # HTTP通信のリクエストに追加処理を加える部分 
│└── middleware.go 
│
├── miroapi/  # 外部APIとの通信処理      
│└── miroapi.go  
│
├── model/       # データベースのテーブル部分を書く      
│└── board_summary.go 
│
├── repository/       
│└── boardrepository.go 
│└── stickyrepository.go 
│
├── usecase/           
│└── usecase.go 
│
├── validator/   # 入力データの検証を行う。APIに渡されるデータが適切かどうか、
│└── validator.go 
└── go.mod          
```
層ごとに分けると、こうなります。
(最も内側)① エンティティ層         → model（ビジネスルールを表現）
           ↓
        ② ユースケース層          → usecase（アプリケーションのビジネスロジック）
           ↓
        ③ インターフェース層      → controller, validator （APIの受け口）
           ↓
(最も外側)④ インフラストラクチャ層  → repository, db, miroapi, middleware, config
（データベースや外部サービス）

依存関係は、controller → Usecase → Repositoryのように（内側から外側）に向けるのが、理想的とされます。これらについて解説します。

# Controllerの役割
HTTP通信で、クライアントからのリクエスト（get,POST）を受け取ります。リクエストボディやURLパラメータからデータを取り出し、適切なデータ構造に変換する。そのリクエストに応じて、ユースケースを呼び出して、処理を任せる。ユースケースからの結果を受け取って、HTTPレスポンスとして、適切な形式（JSONなど）で返す。

```controller.go 
package controller

import (
	"net/http"

	"github.com/matthewTechCom/progate_hackathon/usecase"
	"github.com/matthewTechCom/progate_hackathon/validator"
	"github.com/labstack/echo/v4"
)

type WidgetControllerInterface interface {
	ProcessBoard(ctx echo.Context) error
}

type WidgetRequest struct {
	BoardID     string `json:"boardID" validate:"required"`
	AccessToken string `json:"accessToken" validate:"required"`
}

type WidgetController struct {
	Usecase   usecase.WidgetUsecaseInterface
	Validator validator.ValidatorInterface
}

// コンストラクタ
func NewWidgetController(uc usecase.WidgetUsecaseInterface, v validator.ValidatorInterface) WidgetControllerInterface {
	return &WidgetController{
		Usecase:   uc,
		Validator: v,
	}
}

func (c *WidgetController) ProcessBoard(ctx echo.Context) error {
	req := new(WidgetRequest)
	if err := ctx.Bind(req); err != nil {
		return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "リクエストのフォーマットが不正です"})
	}
	if err := c.Validator.Validate(req); err != nil {
		return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "無効なリクエストパラメータ"})
	}

	// ユースケースの実行:miroのボードから情報を取得してPostgreSQLに保存する
	savedIDs, err := c.Usecase.ProcessAndSave(req.BoardID, req.AccessToken)
	if err != nil {
		return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
	}

	return ctx.JSON(http.StatusOK, map[string]interface{}{
		"message": "情報をDBに保存しました",
		"ids": savedIDs,
	})
}

```
usecase、ValidatorのinterfaceにControllerを依存させることで、それぞれの具体的な実装に依存せず、メソッドを使えるようにしています。

# Usecaseの役割
エンティティと合わせて、アプリケーションの具体的な処理を記述部分です。
repositoryに依存させ、データベースの具体的な処理はrepositoryのinterfaceに書いてあるメソッドを呼び出して、保存処理を行っています。
今回は、「Miroボードから付箋のテキストを取得し、その情報をデータベースに保存する」という処理を記述しました。

```board_summary_usecase.go
// usecase/widget_usecase.go
package usecase

import (
	"fmt"
	"strings"

	"github.com/matthewTechCom/progate_hackathon/model"
	"github.com/matthewTechCom/progate_hackathon/miroapi"
	"github.com/matthewTechCom/progate_hackathon/repository"
)

type WidgetUsecaseInterface interface {
	ProcessAndSave(boardID, accessToken string) ([]int, error)
}

type WidgetUsecase struct {
	BoardRepo  repository.BoardRepositoryInterface
	StickyRepo repository.StickyRepositoryInterface
	MiroAPI    miroapi.MiroAPIInterface
}

func NewWidgetUsecase(boardRepo repository.BoardRepositoryInterface, stickyRepo repository.StickyRepositoryInterface, miro miroapi.MiroAPIInterface) WidgetUsecaseInterface {
	return &WidgetUsecase{
		BoardRepo:  boardRepo,
		StickyRepo: stickyRepo,
		MiroAPI:    miro,
	}
}

func (u *WidgetUsecase) ProcessAndSave(boardID, accessToken string) ([]int, error) {
	// DBに存在するかチェックし、なければ新規保存する
	board, err := u.BoardRepo.GetByMiroID(boardID)
	if err != nil {
		// 存在しない場合、新たな board を保存
		newBoard := &model.Board{
			MiroBoardID: boardID,
			Title:       "",
			Description: "",
		}
		boardIDInt, err := u.BoardRepo.Save(newBoard)
		if err != nil {
			return nil, fmt.Errorf("boardの保存に失敗: %v", err)
		}
		board = &model.Board{ID: boardIDInt, MiroBoardID: boardID}
	}

	// Miro APIからウィジェット情報を取得
	widgets, err := u.MiroAPI.GetWidgets(boardID, accessToken)
	if err != nil {
		return nil, fmt.Errorf("miro APIから情報取得に失敗: %v", err)
	}

	var stickies []*model.Sticky
	for _, widget := range widgets {
		// ここでは widget.Text に「改善」または「反省」が含まれている場合のみ保存する
		var category string
		if strings.Contains(widget.Text, "改善") {
			category = "改善点"
		} else if strings.Contains(widget.Text, "反省") {
			category = "反省点"
		} else {
			// 該当しない場合はスキップ
			continue
		}

		sticky := &model.Sticky{
			BoardID:      board.ID,
			MiroStickyID: widget.ID,
			Content:      widget.Text,
			Category:     category,
		}

		stickies = append(stickies, sticky)
	}

	// 複数の付箋を保存
	savedIDs, err := u.StickyRepo.Save(stickies)
	if err != nil {
		return nil, fmt.Errorf("stickyの保存に失敗: %v", err)
	}

	return savedIDs, nil
}

```


# Repositoryの役割
データベースに関する具体的な記述を書く部分です。
テーブル設計は以下のように行いました。

```model.go
package model

import "time"

// ボード情報を表す
type Board struct {
	ID          int       `json:"id"`  // 参照されるやつ
	MiroBoardID string    `json:"miro_board_id"` // MiroのボードID
	Title       string    `json:"title"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"created_at"`
}

// 付箋の詳しい情報を表す
type Sticky struct {
	ID           int       `json:"id"`
	BoardID      int       `json:"board_id"`       // BoardのIDを参照する（外部キー）
	MiroStickyID string    `json:"miro_sticky_id"` // Miroの付箋ID
	Content      string    `json:"content"`
	Category     string    `json:"category"`       // 改善 or 反省
	CreatedAt    time.Time `json:"created_at"`
}
```
なので、Boardテーブルに関するSQL処理(boardrepository.go)とStickyテーブルに関するSQL処理(stickyrepository.go)の2つを記述しました。

```boardrepository.go
package repository

import (
	"database/sql"
	"github.com/matthewTechCom/progate_hackathon/model"
)

type BoardRepositoryInterface interface {
	Save(board *model.Board) (int, error)
	GetByMiroID(miroBoardID string) (*model.Board, error)
}

type BoardRepository struct {
	DB *sql.DB
}

// コンストラクタ
func NewBoardRepository(db *sql.DB) BoardRepositoryInterface {
	return &BoardRepository{DB: db}
}

// ボード情報を保存する
func (r *BoardRepository) Save(board *model.Board) (int, error) {
	query := `
		INSERT INTO board (miro_board_id, title, description, created_at)
		VALUES ($1, $2, $3, DEFAULT)
		RETURNING id
	`

	var id int
	err := r.DB.QueryRow(query, board.MiroBoardID, board.Title, board.Description).Scan(&id)
	if err != nil {
		return 0, nil
	}
	return id, nil
}

func (r *BoardRepository) GetByMiroID(miroBoardID string) (*model.Board, error) {
	query := `
		SELECT id, miro_board_id, title, description, created_at
		FROM board
		WHERE miro_board_id = $1
	`
	row := r.DB.QueryRow(query, miroBoardID)
	var board model.Board
	err := row.Scan(&board.ID, &board.MiroBoardID, &board.Title, &board.Description, &board.CreatedAt)
	if err != nil {
		return nil, err
	}
	return &board, nil
}


```

```stickyrepository.go
package repository

import (
	"database/sql"
	"github.com/matthewTechCom/progate_hackathon/model"
)

type StickyRepositoryInterface interface {
	Save(sticky []*model.Sticky) ([]int, error)
}

type StickyRepository struct {
	DB *sql.DB
}

func NewStickyRepository(db *sql.DB) StickyRepositoryInterface {
	return &StickyRepository{DB: db}
}

func (r *StickyRepository) Save(stickies []*model.Sticky) ([]int, error) {
	var savedIDs []int

	for _, sticky := range stickies {
		query := `
			INSERT INTO sticky (board_id, miro_sticky_id, content, category, created_at)
			VALUES ($1, $2, $3, $4, DEFAULT)
            RETURNING id
		`
		var id int
		err := r.DB.QueryRow(query, sticky.BoardID, sticky.MiroStickyID, sticky.Content, sticky.Category).Scan(&id)
		if err != nil {
			return nil, err
		}
		savedIDs = append(savedIDs, id)
	}

	return savedIDs, nil

}

```


# まとめ
今回、Go言語処理のクリーンアーキテクチャでの実装、lambda関数により自動化処理、外部APIの使用、データベースへの保存処理など、様々なことを学べました。
次なる目標は、
- 自分のアイデアを形にする：すでにアイデアは決まっており、これを実現できるアプリ開発に取り組みます。
- ハッカソンで優秀賞を目指す：完成度の高いアプリケーションを作り、コンペティションで優秀賞を獲得できるように努力します。

ありがとうございました。

作ったやつのコード：https://github.com/annyouu/go-hack-backend/tree/main/backend