Go でクイズアプリを作りながらクリーンアーキテクチャに入門した
はじめに
最近 Go でクリーンアーキテクチャをやる本を読んだので、実際にクリーンアーキテクチャでオリジナルのクイズアプリを作ってみました。クリーンアーキテクチャの話は n 番煎じだと思いますが、自分の中での理解と感想をまとめておこうと思います。また、今回 Web アプリとして動かすためにフロントエンドも作りましたが、こちらはクリーンアーキテクチャとは特に関係ないので本記事では触れません。
読んだ本はこちら👇 (最近 書籍版 も出たようです)
つくったもの
今回つくったものは簡単なクイズアプリで、クイズの回答/投稿/削除、回答数/正答率の表示、ログインなどの機能を実装しました。他にもタグ付け機能、検索機能、Markdown 対応など色々やろうとしてたのですが、ここまで作って力尽きてきたので一旦完成として追々機能追加して行きたいなと思っています。
以下からアクセスできるので是非遊んでいってください!
技術スタック
- フロントエンド
- 言語: TypeScript
- FW: Next.js
- バリデーション: Zod
- フォーム: React Hook Form
- スタイル: shadcn/ui, Tailwind CSS
- 認証: Auth.js
- デプロイ先: Vercel
- バックエンド
- 言語: Go
- FW: Echo
- DB: PostgreSQL
- ORM: sqlc
- デプロイ先: Render
クリーンアーキテクチャについて
クリーンアーキテクチャについて超ざっくり解説すると、重要なポイントは
- 中央にいるレイヤーほど重要で変化が起きにくい
- 依存の矢印は常に外側から内側に向かうようにする
という 2 つです。
ただ愚直に実装していくと、例えば DB 操作を行う repository (インフラ層) の実装を usecase (アプリケーション層) で使用 (依存) するような実装が発生し、依存の向きが図とは逆向きになる問題にぶつかります。そこでクリーンアーキテクチャではこの問題を解決するために、依存性逆転の原則(Dependency Inversion Principle)を使います。具体的にはインターフェースを間に入れることで、下図のように usecase も repository もインターフェースに依存する形になり、インターフェースは呼び出し側で定義しているものなので実質的に依存の向きを逆転させることができます。
また、依存性の逆転によりモジュールの差し替えも簡単になります。今回の場合 repository 側はインターフェースさえ満たせば、usecase 側に一切手を入れずに異なる DB に差し替えることもできますし、インターフェースを満たした repository のモックを⽤意 (ツールで生成) するだけで usecase のユニットテストを⾏うことができます。
実際に開発してみての気づき
書くべきコード量が多い
作る前から想像はしてましたが、やはり依存関係を意識するとクラス (構造体) やパッケージを適切に分割していく必要があり、レイヤー間でデータ構造の変換が必要になるため、全体としてコード量は増えました。特に配列のようなデータを取得する場合、レイヤーを跨ぐたびにドメインオブジェクトや DTO (Data Transfer Object) などにデータ変換しながら何回も詰め直すみたいなことをする必要があり、冗長なコードになりがちでした。
MySQL から PostgreSQL への移行もスムーズ
開発初期は書籍で MySQL を使用していたのでそのまま同じように MySQL で進めていたのですが、ある程度開発が進み、いざどこかにデプロイしようと思ったら、現状無料 (かつクレカ登録不要) で MySQL が使える DB サービスが無さそうだったため PostgreSQL に変更したくなりました。
ただ、前述の通りクリーンアーキテクチャで設計しておくことで、このようなインフラ層の差し替えに関しても他の層を意識することなく簡単に行うことができました。また、クリーンアーキテクチャとは直接関係ないですが今回 sqlc を使って SQL からコード生成を行なっていたため、実質スキーマとクエリを差し替えるだけでほとんど対応することができました。
Repository パターンで N+1 問題が起きる
今回つくったアプリの場合、クイズ一覧を表示する際にクイズ本文と合わせて作者を表示するためにユーザ情報も同時に取得する必要があったのですが、Repository パターンだとクイズとユーザーのようなドメインを跨ったデータをまとめて取得するようなことはできないため、結果的にクイズごとにユーザ情報を取得するクエリが発行され N+1 問題が発生してしまいました。
そこで、この問題の有効な解決策として CQRS (Command Query Responsibility Segregation) パターンを使いました。CQRS とはコマンド(更新系)とクエリ(参照系)を分離しましょうというパターンで、具体的には Repository と合わせて QueryService と呼ばれる参照系限定でドメインを跨ってデータを返せるルートを用意します。QueryService を使うと DB クエリ内で結合したデータをまとめて返せるので上記の N+1 問題を解決できましたが、アーキテクチャ的には複雑度は増すので用法用量は守る必要がありそうです。
さいごに
今回、クリーンアーキテクチャの本を読んで簡単なアプリを作ってみましたが、実際に手を動かすとなんとなく理解したつもりになっていた部分で躓くので、より理解が深まった気がします。特に、今まで依存の向きなどあまり真面目に考えたことがなかったので言語機能の interface のメリットがいまいちしっくりきていなかったのですが、クリーンアーキテクチャを勉強するとだいぶ腹落ち感がありました。次はクリーンアーキテクチャの 定番本 を読んだり DDD なども勉強していきたいです!
Discussion