1. はじめに
今回、Goを使ったAPIサーバーを構築しようとした際、アーキテクチャの知識やテストの方法などについて調べる機会があったので、忘備録も兼ねてこの機会にまとめようと思います。
アーキテクチャにも触れますが、今回の主題はechoとmongoを使った環境構築とテストですので、アーキテクチャが気になった方は他の記事も参考にしてください。
今回のリポジトリです。
少々フォルダ構成が異なっていますがアーキテクチャ含め大部分は同じです。
2. 環境構築
今回はDockerにて環境構築を行います。Dockerに関する解説は以前別記事でも行いましたので、もしDockerの詳細が気になる方がいらっしゃればそちらも参考にしてください。他の方の記事を見てもいいと思います。
今回のフォルダ構成としては以下のようにしています。
C:.
│ .env
│ compose.yaml
│ Dockerfile
│
└─work
│ go.mod
│ go.sum
│
├─app
│ main.go
│
├─domain
│ receipt.go
│ receipt_test.go
│
├─infrastructure
│ db.go
│ receipt.go
│
├─interface
│ controllers.go
│ receipt.go
│ receipt_test.go
│
└─usecase
receipt.go
receipt_test.go
compose.yamlファイルやDockerファイル、そして環境変数の入ったファイルはルートディレクトリーに配置しており、その下にGoのファイルを記述したworkフォルダーを配置しています。
2-1. Go
まず、GoをDockerで環境構築します。
FROM golang
WORKDIR /work
COPY ./work /work
次に、compose.yamlファイルを記述します。のちに、ここでmongodbの記述も行いますが、ひとまずはGoだけに専念します。
services:
app:
container_name: "go-mongodb"
build:
context: .
dockerfile: Dockerfile
tty: true
environment:
TZ: Asia/Tokyo
ports:
- 8080:8080
volumes:
- type: bind
source: ./work
target: /work
これでGoの環境構築は終了しました。
docker compose up -d -build
コマンドを打てば、プロジェクトが起動します。
Goのコマンドを実行するには、docker compose exec <サービス名> sh
などとしてコンテナーの中に入るか、docker compose exec <サービス名> go <コマンド>
などとしてコンテナーにコマンドを実行させる方法があります。
echoをインストールするためのコマンドは以下の通りです。
go mod init <プロジェクト名>
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
2-2. mongodb
mongodbはcompose.yamlファイル上の記述だけで完結させられますが、環境変数のファイルへの直書きは避けたいので、.envファイルから取得するようにします。この際、Goでもその環境変数を用いるので先ほど定義したappサービスの方にも環境変数を渡すような記述をしてあげます。
services:
app:
container_name: "go-mongodb"
build:
context: .
dockerfile: Dockerfile
tty: true
environment:
+ MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME}
+ MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
TZ: Asia/Tokyo
ports:
- 8080:8080
volumes:
- type: bind
source: ./work
target: /work
mongo:
image: mongo
container_name: mongodb
hostname: mongodb
restart: always
ports:
- 27017:27017
environment:
MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
TZ: Asia/Tokyo
volumes:
- mymongodb:/data/db
- mymongoconfig:/data/configdb
volumes:
mymongodb:
mymongoconfig:
volumesはデータベースのデータを永続化するために使われるディレクトリーで、ライフサイクルがコンテナーと異なっているためコンテナーが削除されてもデータベースのデータは生き残ります(ボリュームマウント)。
これに対し、ホストディレクリーにコンテナー内のディレクトリーをバインドすることで、どちらかでの編集がもう片方の環境にも反映させるようにすることもできます。(バインドマウント)
appサービスにて使っているvolumesはバインドマウントですが、mongoサービスにて使っているvolumesはボリュームマウントです。データベースのデータの永続化は、バインドマウントではなくボリュームマウントがお勧めされます。この記事にてボリュームマウントの方が安全に複数のコンテナー間でデータを共有できるメリットなどが紹介されているとおり、データの永続化にはこちらの方が向いていると思われます。
mongodbを立ち上げられたのはよかったのですが、私の場合mongodbのコンテナーにコマンドを実行させるのが思ったより手間取りました。
色々調べた結果、こちらの記事に辿り着き、コンテナー内に入ってコマンドを実行した方が確実そうだと思いました。
以下、mongodbのコンテナーに入り、コマンドを実行させる流れです。
まずコンテナーに入ります。
docker exec -it mongodb-todo /bin/sh
次に、mongodbに接続します。
# mongosh --host localhost --port 27017 --username root \
--password password --authenticationDatabase admin
正常に接続できれば、自分で定めたデータベース名でデータベースにアクセスし、プロジェクトで作ったコレクションに対して各種メソッドを実行させることができます。
# use <database>
# db.<collection>.find()
上の例では、データベースに登録されてあるデータが一覧で表示されます。
以上で環境構築と、データの確認方法を紹介しました。
mongodbの認証とvolumes
compose.yamlファイルにてMONGODB_INITDB_ROOT_USERNAMEとMONGODB_INITDB_ROOT_PASSWORDを設定しましたが、これはdocker compose down
コマンドを実行しても削除されず、envファイルに他の値を設定した場合mongodbへの認証を受け付けられなくなる可能性があります。これはおそらくデータボリュームによって初期値が保存されているためだと考えられます。docker compose down --volumes
コマンドを実行するとこでデータの初期化が行えるはずです。
3. レイヤードアーキテクチャとDDD
私はこれまで、特にアーキテクチャを意識したことはなかったのですが、大きめのシステムを作る際には特定の原則に沿ってフォルダ構成を考えなければ複雑化に耐えられないため、今回の開発ではアーキテクチャも併せて学びました。
この記事ではechoとmongodbの実装とテストを中心に紹介するのですが、テストを行うにあたってフォルダ構成や層ごとの依存関係がわかっていないと雰囲気しかつかめないかもしれないと考え、フォルダ構成とその背景知識としてのアーキテクチャもここで紹介させていただく次第です。
とはいえ、既にわかっている方には不要だと思うので、テストの方法だけ知りたい方は次の章へお進みください。
この章の記述はこの記事を参考にさせていただきました。
3-1. レイヤードアーキテクチャ
レイヤードアーキテクチャはソフトウェアシステムを複数の層に分けて構造化する方法です。各層はそれぞれ明確な役割を持ち、依存関係を一方通行になるようにします。これにより、システムの複雑さを軽減し、各部分の独立性を高めることができます。
- interface
- フロントエンドからリクエストを受け取り、レスポンスを返します。ルーティング機能はここで実装されることになるでしょう
- usecase
- domainのモデルを用いて何をするかということを記述します。
- domain
- システムの中核となる部分で、ビジネスのルールや概念をモデル化します。エンティティ、サービスなどのドメインモデルで構成されます。
- infrastructure
- 技術的関心事を記述します。データベース操作や外部サービスとのやり取りの責任を持ちます。
3-2. DDD
DDDは、複雑なビジネスドメインをソフトウェアに落とし込むための設計手法です。ドメインモデルを中核に据え、ビジネスの専門家と開発者が密接に連携しながら、ソフトウェアを開発していきます。
レイヤードアーキテクチャとDDDを組み合わせることで、ドメイン層が中心になるような開発をします。
インフラストラクチャー層とドメイン層の依存関係が逆転したのがわかるでしょうか。
この形式の開発手法では、ドメインが中心となることになります。
3-3. 実装の具体例
今回の実装では、伝票を受け取るような処理を書きます。
まず、ドメイン層です
package domain
import (
"fmt"
)
type Receipt struct {
Date string
Category string
Content string
Money int
Remarks string
}
func (r Receipt) Validate() error {
if r.Money <= 0 {
return fmt.Errorf("金額を正しく入力してください")
}
if r.Date == "" || r.Category == "" || r.Content == "" {
return fmt.Errorf("必要なデータを入力してください")
}
return nil
}
type ReceiptRepository interface{
Create(receipt *Receipt) error
}
ここでは、ReceiptエンティティとReceiptRepositoryというリポジトリ―を定義しています。エンティティは一意な識別子を持ち、状態を持つオブジェクトで、リポジトリ―はドメインオブジェクトの永続化を担当します。
ビジネスロジックにだけ集中したいので、データの永続化を担当するReceiptRepositoryにはインターフェースだけ定義し、インフラストラクチャー層に任せます。
次にインフラストラクチャー層です。
package infrastructure
import (
"context"
"work/domain"
)
type ReceiptRepositoryInfrastructure struct {
db *MyMongoDB
ctx context.Context
}
func NewReceiptRepositoryInfrastructure(db *MyMongoDB, ctx context.Context) domain.ReceiptRepository {
return &ReceiptRepositoryInfrastructure{db, ctx}
}
func (r *ReceiptRepositoryInfrastructure) Create(receipt *domain.Receipt) error {
_, err := r.db.Client.Database("mongo").Collection("receipt").InsertOne(r.ctx, receipt)
if err != nil{
return err
}
return nil
}
ここではデータベースとのやり取りを記述しています。
ReceiptRepositoryInfrastructureという構造体のインスタンスを返すコンストラクター関数を定義しており、その返り値の型にはReceiptRepositoryを指定しています。ここで、ReceiptRepositoryInfrastructureにはCreateメソッドを定義しているので、型の検証をパスすることができます。これにより呼び出し先で、ReceiptRepositoryインターフェースの要件を備えたものとして扱うことができます。
このような依存性注入の話は前の記事で詳しく解説いたしましたので、気になる方はそちらもご覧ください。
この依存性注入の処理により、この構造体の受取先は構造体に直接依存するのではなく、リポジトリのインターフェースに依存することになり、疎結合が実現されます。
また、ここの層でmongodbとの接続処理も記述しておきます。
Goのデータベース接続処理を記述するのに先立って、先にドライバーをインストールしておきます。
go get go.mongodb.org/mongo-driver/mongo
このパッケージを使ってmongoのデータベースに接続できるようになりました。
以下がコードの中身です。
package infrastructure
import (
"context"
"fmt"
"os"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MyMongoDB struct{
Client *mongo.Client
}
func (db *MyMongoDB) getUri() (uri string) {
username := os.Getenv("MONGO_INITDB_ROOT_USERNAME")
password := os.Getenv("MONGO_INITDB_ROOT_PASSWORD")
uri = fmt.Sprintf("mongodb://%s:%s@mongo:27017",username,password)
return uri
}
func (db *MyMongoDB) Connect(ctx context.Context) (err error) {
opt := options.Client().ApplyURI(db.getUri())
if err := opt.Validate(); err != nil{
return err
}
db.Client, err = mongo.Connect(ctx, opt)
return err
}
func (db *MyMongoDB) Disconnect(ctx context.Context) error {
return db.Client.Disconnect(ctx)
}
func (db *MyMongoDB) Ping(ctx context.Context) error {
if err := db.Client.Ping(ctx, nil); err != nil{
return err
}
fmt.Println("Successfully connect")
return nil
}
Pingメソッドはデータベースとの接続ができたかを確認するものです。確認する必要がなければ、なくても大丈夫です。
ここの記述で一つ戸惑ったのが、uriの指定の方法です。
最初はURIをlocalhost:27017
としていたのですが、compose.yamlファイルで環境構築した場合は少し勝手が違うようでした。この記事によると、localhostではなくてmongodbのサービス名を指定する必要があるとのことでした。
今回のcompose.yamlファイルではmongoというサービス名を使っているのでmongo:27017
となっています。
次にユースケース層です。
package usecase
import (
"strconv"
"work/domain"
)
type ReceiptUsecase interface{
CreateReceipt(input *CreateReceiptInput) error
}
type CreateReceiptInput struct{
Date string `json:"date" form:"date"`
Category string `json:"category" form:"category"`
Content string `json:"content" form:"content"`
Money string `json:"money" form:"money"`
Remarks string `json:"remarks" form:"remarks"`
}
type receiptUsecase struct {
receiptRepository domain.ReceiptRepository
}
func NewReceiptUsecase(rp domain.ReceiptRepository) ReceiptUsecase{
return &receiptUsecase{ receiptRepository: rp }
}
func (r *receiptUsecase) CreateReceipt(input *CreateReceiptInput) error {
money, err := strconv.Atoi(input.Money)
if err != nil{
return err
}
receipt := &domain.Receipt{
Date: input.Date,
Category: input.Category,
Content: input.Content,
Money: money,
Remarks: input.Remarks,
}
if err := receipt.Validate(); err != nil {
return err
}
if err := r.receiptRepository.Create(receipt); err != nil {
return err
}
return nil
}
ここではインターフェース層に提供するメソッドを定義しつつ、ドメイン層で定義されたリポジトリを用いて受け取ったデータの永続化を図ります。
リポジトリインターフェースに定義されたCreateメソッドを使っていますが、ユースケース層ではその実装の中身を意識せずに(具体的な実装の中身はインフラストラクチャー層に一任しています)使うことができます。
このように「関心の分離」ができていること、即ち「今は他の事(データベースとのやり取り)を考えずに関心事(ユースケース層でいえば、データを受け取ってデータベースに渡す前の処理、ビジネスロジックの制御)だけに集中できる」というのがレイヤーに分けるメリットです。
次にインターフェースの実装です。
package interfaces
import (
"github.com/labstack/echo/v4"
)
type Controllers struct {
receiptController *ReceiptController
}
func NewControllers(rc *ReceiptController) *Controllers {
return &Controllers{ receiptController: rc }
}
func (c *Controllers) Mount(e *echo.Echo) {
c.receiptController.Mount(e.Group("/receipt"))
}
ここでは、登録されたコントローラーをまとめてマウントする処理を書いています。今回は伝票に関わる処理しかありませんが、ほかの機能も加わった場合はこの構造体のメンバーやコンストラクター関数の引数が増えていくことでしょう。
ReceiptControllerの中身は以下のようになっています。
package interfaces
import (
"net/http"
"work/usecase"
"github.com/labstack/echo/v4"
)
type ReceiptController struct {
receiptUsecase usecase.ReceiptUsecase
}
func NewReceiptController(ru usecase.ReceiptUsecase) *ReceiptController{
return &ReceiptController{receiptUsecase: ru}
}
func (c *ReceiptController) Mount(g *echo.Group) {
g.POST("", c.Create)
}
func (c *ReceiptController) Create(e echo.Context) error{
receipt := new(usecase.CreateReceiptInput)
if err := e.Bind(receipt); err != nil{
return e.String(http.StatusBadRequest, "入力値が不正確です")
}
err := c.receiptUsecase.CreateReceipt(receipt)
if err != nil{
return e.String(http.StatusInternalServerError, "データベースへの登録に失敗しました")
}
return e.String(http.StatusCreated, "登録完了しました")
}
ここでは、/receiptというパスに対するルーティング機能を実装しています。
CreateメソッドはechoのPOSTメソッドに登録するハンドラー関数です。
最後に、main関数を紹介して終わります。
package main
import (
"context"
"log"
"net/http"
"time"
"work/infrastructure"
"work/interface"
"work/usecase"
"github.com/labstack/echo/v4"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mydb := new(infrastructure.MyMongoDB)
if err := mydb.Connect(ctx); err != nil{
log.Fatal(err)
}
defer mydb.Disconnect(ctx)
if err := mydb.Ping(ctx); err != nil{
log.Fatal(err)
}
e := echo.New()
e.GET("/", func(c echo.Context)error{return c.String(http.StatusOK, "hello")})
receiptRepository := infrastructure.NewReceiptRepositoryInfrastructure(mydb, ctx)
receiptUsecase := usecase.NewReceiptUsecase(receiptRepository)
receiptController := interfaces.NewReceiptController(receiptUsecase)
controllers := interfaces.NewControllers(receiptController)
controllers.Mount(e)
e.Logger.Fatal(e.Start(":8080"))
}
4. テスト
最後に、このプロジェクトにおいて、どのようにテストを実行したかを紹介します。
4-1. Goにおけるテスト
まず、Goではどのようにテストを実行するかを紹介します。
とはいっても、Goの組み込みのパッケージでテストは行えるので、比較的楽だと思います。
domainのreceipt.goのテストファイルを作ることを例に紹介します。
まずは、receipt_test.goというファイル名でgoファイルを作成します。
次に、testingパッケージをインポートしてテストを実行する関数を作りますが、これはTest_xxxやTestXxxの形で関数を書かなければテスト関数とは認識してくれない点に注意です。また、テスト関数にはtesting.T型の値をポインタレシーバで引数に取るようにしなければなりません。
以下が具体的な例です。
package domain
import "testing"
func Test_Validate_Success(t *testing.T) {
receipt_test := Receipt{
Date: "2024/09/01",
Category: "meal",
Content: "A portion of dinner",
Money: 500,
}
err := receipt_test.Validate()
if err != nil{
t.Fatal("Failed test")
}
}
func Test_Validate_Failure(t *testing.T) {
receipt_test := Receipt{
Date: "2024/09/01",
Category: "meal",
Content: "A portion of dinner",
}
err := receipt_test.Validate()
if err == nil{
t.Fatal("Failed test")
}
}
実際に関数を呼び出し、結果を検証することによってテストが成功したかどうかを判定します。
テストコマンドは以下の通りです。
go test -v work/usecase // パッケージのパス
これ以外にもテストコマンドはあるようなので、上記の例ではできなかった方は他のも探してみてみてください。
4-2. mongodbの代わりにモックを使ったテスト
4章の二節と次の節が、この記事を書こうと思った一番のモチベーションでした。特にmongodbでモックを作ってテストするにはどうしたらよいかということがなかなか出てこず、かなり悪戦苦闘しました。
先に結果だけお示しすると、以下のコードになります。
package usecase
import (
"context"
"testing"
"work/infrastructure"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func Test_CreateReceipt_Success(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("insert one", func(mt *mtest.T) {
cri := &CreateReceiptInput{
Date: "2029/09/01",
Category: "meal",
Content: "A portion of dinner",
Money: "500",
}
ctx := context.Background()
// モックのmongo.Clientを直接渡す
mydb := &infrastructure.MyMongoDB{Client: mt.Client}
mt.AddMockResponses(mtest.CreateSuccessResponse())
// addMockResponsesは位置関係が大切
receiptRepository := infrastructure.NewReceiptRepositoryInfrastructure(mydb, ctx)
receiptUsecase := NewReceiptUsecase(receiptRepository)
if err := receiptUsecase.CreateReceipt(cri); err != nil{
t.Fatal("The create process failed.")
}
})
}
func Test_CreateReceipt_Failure(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("insert one", func(mt *mtest.T) {
cri := &CreateReceiptInput{
Date: "2029/09/01",
Category: "meal",
Content: "A portion of dinner",
}
ctx := context.Background()
mydb := &infrastructure.MyMongoDB{Client: mt.Client}
mt.AddMockResponses(mtest.CreateSuccessResponse())
receiptRepository := infrastructure.NewReceiptRepositoryInfrastructure(mydb, ctx)
receiptUsecase := NewReceiptUsecase(receiptRepository)
if err := receiptUsecase.CreateReceipt(cri); err == nil{
t.Fatal("The create process failed.")
}
})
}
結論から言えば、mongo-driverにもとから備わっていたmtestを使うことになりました。
mtest.Newメソッドを呼び出してmtインスタンスを獲得し、mt.RUNメソッドでデータベースのモックを起動します。
このコードで大切なのは、mt.AddMockResponses()というメソッドをデータベースのインスタンス化の後に渡さなければならないということです。これより前にこのメソッドを実行していると、nilポインタによるエラーみたいなのが吐き出されました。
ここではCreateメソッドの確認だけしており、そのためデータベースへのデータの挿入処理が成功したという結果だけ返させていますが、Findメソッドなどを使いたいときはもう一工夫必要です。
AddMockResponsesの引数には以下のように具体的なデータ例を入れることになります。
mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch,
bson.D{
{Key: "_id", Value: "abcdefg"},
{Key: "Date", Value: "2024/09/08"},
{Key: "Category", Value: "meal"},
{Key: "Content", Value: "Chiken and Rice"},
{Key: "Money", Value: 100},
{key: "Remarks", value: "A portion of meal, with alumini of the society"},
}))
mongodbとのテストの方法は、この方の記事に詳しいのでぜひ参考になさってください。
4-3. ハンドラー関数のテスト
最後に、ハンドラー関数のテストを紹介して終わります。
APIのテストをするにあたって、データベースのインスタンス化が必要になるので、テスト関数の最初の方はユースケース層のテストファイルと似た記述になります。
package interfaces
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"work/infrastructure"
"work/usecase"
"github.com/labstack/echo/v4"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)
func Test_Create_Success(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("insert one", func(mt *mtest.T) {
ctx := context.Background()
mydb := &infrastructure.MyMongoDB{Client: mt.Client}
mt.AddMockResponses(mtest.CreateSuccessResponse())
receiptRepository := infrastructure.NewReceiptRepositoryInfrastructure(mydb, ctx)
receiptUsecase := usecase.NewReceiptUsecase(receiptRepository)
receiptController := NewReceiptController(receiptUsecase)
reqBody := strings.NewReader(`{
"date": "2029/09/01",
"category": "meal",
"content": "A portion of dinner",
"money": "500"
}`)
e := echo.New()
e.POST("/receipt", receiptController.Create)
req := httptest.NewRequest(http.MethodPost, "/receipt", reqBody)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("Expected status code 201, but got %d", rec.Code)
}
if body := rec.Body.String(); body != "登録完了しました" {
t.Errorf("Expected response body '登録完了しました', but got '%s'", body)
}
})
}
func Test_Create_Failure(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
mt.Run("insert one", func(mt *mtest.T) {
ctx := context.Background()
mydb := &infrastructure.MyMongoDB{Client: mt.Client}
mt.AddMockResponses(mtest.CreateSuccessResponse())
receiptRepository := infrastructure.NewReceiptRepositoryInfrastructure(mydb, ctx)
receiptUsecase := usecase.NewReceiptUsecase(receiptRepository)
receiptController := NewReceiptController(receiptUsecase)
reqBody := strings.NewReader(`{
"date": "2029/09/01",
"category": "meal",
"content": "A portion of dinner",
"money": "0"
}`)
e := echo.New()
e.POST("/receipt", receiptController.Create)
req := httptest.NewRequest(http.MethodPost, "/receipt", reqBody)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("Expected status code 500, but got %d", rec.Code)
}
if body := rec.Body.String(); body != "データベースへの登録に失敗しました" {
t.Errorf("Expected response body 'データベースへの登録に失敗しました', but got '%s'", body)
}
})
}
ハンドラー関数のテストの方法は幾つかあるようです。
今回はhttptestのリクエストとレコーダーをechoのServeHTTPメソッドに渡すことでテストを行いましたが、httptestのNewServerメソッドを使う方法や、echo.EcoのインスタンスのNewContextメソッドにリクエストとリコーダーを渡してコンテキストを取得してテストする方法などもあるようです。
この方の記事にそれぞれのテスト方法の解説がのっているのでぜひ参考にしてください。
5. おわりに
全体的に、これまで仕入れた知識を広く浅く整理するような形になった気がします。
最低限、其々の知識やコードの書き方などのノウハウは獲得できましたが、他のアーキテクチャやテストの効率的な書き方など、まだまだ知るべきことは沢山あるなと感じました。これを機に、まだまだ学習を進めていきたいと思います。
結構長い記事になったこともあり、ところどころ解説が雑になったところもあったかもしれません。
この記事の内容を補完したり参考元を記載したりするために参照は都度掲示しているので、ここで分からなかったらそちらをご参照していただくことをお勧めします。
最後に一覧で参考文献をまとめておきます。
6. 参考
第二章
第三章
第四章