strt's blog

🗒🔨🙍

最近勉強していたことの仮まとめ

2025/03/02

色々な事項をキャッチアップしたり、曖昧になっていたところを勉強しなおす機会があったので、ざっと所感とともにまとめておこうと思いました。

ものによっては別途追加で勉強してから書いた方がいいかなと思ったので、詳細はまた別の機会に。

レイヤードアーキテクチャ

基本的に仕事ではMVCフレームワークのAPIモードで作ることが多かったので、あまりアプリケーションのアーキテクチャをどうしようと考える機会が少なかったのですが、Go/echoでAPIサーバを作る際にどうしようと考えた際にちゃんと理解を深める形としました。

主に「プレゼンテーション層」「ビジネスロジック層」「データアクセス層」などアプリケーションを複数の層に分け、それぞれは上位層が下位層に依存する形とするアーキテクチャです。 ただ、DDDの文脈で出てくる際は「プレゼンテーション層」「アプリケーション層」「ドメイン層」「インフラ層」の四層で構成されているものをレイヤードアーキテクチャと呼ぶようです。

明確に層に分かれているため、何をどこに書くべきかをあまり迷わず、自然と整理されていくのは一定メリットに感じます。

結局Go/echoで2パターン試してみましたが、普段自分で作る分にはプレゼンテーション・ビジネスロジック・インフラの3層を準備するので十分そうでした。 DDDを意識していきたかったり、仕事でやる場合は4層くらいに分けて肥大化を防ぐのも有効そうに思います。

Go/echoでの(開発)環境構築

とりあえず書いてみようということで、以下のライブラリを使いつつ環境構築を進めました。

  • air: ホットリロードのため。
  • migrate: gormよりも管理しやすそうなマイグレーションツールのため。

どちらも開発環境ではdocker/docker-composeで以下のようにして立ち上がるようにしました(一部のみ)。

  • Dockerfile
FROM golang:1.23.5-bookworm
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

ENV TZ /usr/share/zoneinfo/Asia/Tokyo
WORKDIR /app
COPY /app/* ./
RUN go mod download
RUN go install github.com/air-verse/air@latest
EXPOSE 1323

CMD ["air", "-c", ".air.toml"]
  • docker-compose.yml
migrate:
	container_name: migration
	image: migrate/migrate
	env_file:
		- .env
	command:
		[
		"-path",
		"/migrations",
		"-database",
		"mysql://root:${MYSQL_ROOT_PASSWORD}@tcp(db:3306)/${MYSQL_DATABASE}",
		"up",
		]
	volumes:
		- ./db/migrations:/migrations
	depends_on:
		db:
			condition: service_healthy

docker-compose.ymlの書き方関連

今までも結構なあなあで書いたりしてたのですが、仕事でここら辺を書き直したりすることも多かったので、いくつか知らなかったけど便利なことを列挙します。

nameの指定

複数のアプリを立ち上げる際に明示的にnameを付けることにより、docker-compose.ymlのあるディレクトリの名前とは関係なく名前をつけられるようになります。

趣味のプロジェクトではフロントとバックエンドを1つのリポジトリで管理する(ルートディレクトリにfrontendディレクトリとbackendディレクトリを用意)ので、名称被っちゃうんですよね・・・

docker-composeでのhealthcheckの指定

あるあるだとは思うんですが、DBのコンテナを先に起動→その後にアプリケーションコンテナを起動/migrationを走らせる、などの起動順を制御したい場合、healthcheckを入れることで御しやすくなります。

mysqlの場合は以下のような形

  • docker-compose.yml
healthcheck:
	test: ["CMD","mysqladmin","ping","-h","$HOST","-u","$USER","-p$PASSWORD",]
	interval: 10s
	timeout: 10s
	retries: 5
	start_period: 10s

postgresqlの場合は以下のような形

  • docker-compose.yml
healthcheck:
	test: ["CMD-SHELL","pg_isready -U $USER -d $DB_NAME || exit 1"]
	interval: 10s
	timeout: 10s
	retries: 5
	start_period: 10s

で設定可能です。

Dockerfileでのマルチステージビルド

なんらかビルドが必要な際に、Dockerfile内にビルド用ステージの記述と、起動用ステージの記述を分けて記載し、ビルド用ステージの成果物をを起動用ステージにコピーすることで、イメージのサイズを縮小する手法です。

開発環境ではあまり必要なさそうではありますが、本番環境用のDockerfileではぜひ使いたいという所感です。

ただ、Prismaを利用する場合、ビルド用ステージで使うDockerイメージと起動用ステージで使うDockerイメージは揃えておいた方がよく、片方がbookworm、片方がalpineなどにしてしまうとうまくいかなかったりもします。 T3 StackでこのドキュメントからDockerfileの中身をコピペしたら引っかかりました。

DIの概要

あるオブジェクトが、依存する他のオブジェクトを外部から受け取る形とする実装の方法で、インターフェース定義を間に挟んで結合度を下げることでテスタビリティの向上を図ったり、拡張しやすくしたりすることを目的としています。 DjangoやRailsを主に使っていたこともあり、あまりDIを意識して開発する機会はなかったのですが、レイヤードアーキテクチャを実施するとともにDIにも触れることになりました。

大体以下のようなイメージです。(Goで記載)

package main
import "fmt"

// 挨拶のインターフェース
type Greeter interface {
	Greet()
}

// 朝の挨拶の具体的実装
type morning struct{}

func (m *morning) Greet() {
	fmt.Println("Good Morning!")
}

// 夕の挨拶の具体的実装
type evening struct{}

func (e *evening) Greet() {
	fmt.Println("Good Evening!")
}

// 挨拶するメソッド(挨拶の内容は引数に依存)
func say(g Greeter) {
	g.Greet()
}

func main() {
	m := &morning{}
	e := &evening{}

	say(m)
	say(e)
}

これにより、say自体のユニットテストはGreeterのMockを作ることで簡単にテストできるようになったり、実装の差し替え・追加などがしやすくなったりします。