0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-03-25

はじめに

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

どういうハッカソンに参加したの?

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

作ったものは?

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

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

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

miroはこれ↓

自分の担当は?

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

自分が作った部分

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

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

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

工夫した点

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

クリーンアーキテクチャって?

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

b1041924432af01ffd983ba5.png

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

重要なポイント

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

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

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

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

ここで、私は、最初つまづきました。。

  • 依存関係の制御にinferfaceが絡んでくる...???
  • そもそもinterfaceってなに???

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

interfaceについて

interfaceは、簡単に言えば、「抽象化」をおこなっています。
例えば、犬、猫、たぬき、うさぎを抽象化すると、「動物」ですよね?
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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?