見出し画像

AI英会話チャット学習サービス作ってみた (テクノロジー編)

こちらで紹介しているサービスの技術面についての紹介です。



全体アーキテクチャ

本サービスの全体アーキテクチャは以下のようになっています。
インフラはAWSを採用し、フロントエンドはReact、バックエンドはNodeをそれぞれTypeScriptベースで作成しました。


フロントエンド周辺アーキテクチャ

フロントエンド周辺のアーキテクチャ
(定番のCloudFront + S3構成)

S3

Reactの章で詳細は説明しますが、費用削減の観点からフロントエンドは静的ホスティングを採用することにしました。

静的ホスティングの配置先ですが、費用面、可用性、扱いやすさの観点から、S3を採用しました。

(AWS社もSPAアプリの公開方法としてS3の利用を推奨しています)

CloudFront

S3のCDNとしてCloudFrontを採用しました。

S3にデプロイされたReactの静的ファイルをCloudFrontでキャッシュさせ、転送コストを抑えることができます。

また、ユーザー目線もキャッシュファイルを取得することで読み込み速度を上げることができ、パフォーマンスを向上させることができるので導入しました。

Route53

AWSが提供しているマネージド型のDNSサーバーです。

ドメインの取得からレコードの作成まで自動で行ってくれたり、Aliasレコードを利用することで、AWSサービスとの連携を省略できる完結さから採用しました。

バックエンド周辺アーキテクチャ

バックエンド周辺のアーキテクチャ
(サーバレスアプリケーションモデルを活用)

Lambda

バックエンドアプリを配置するサーバはLambdaを利用します。

従量課金制のため、個人開発でそこまでトラフィック量が多くならないことを想定して、費用面を抑えられるLambdaを採用することにしました。
(無料枠も広いので小規模でバックエンドを建てるときはお勧めです!)

また、サーバレスでOSやスペック周りのことを管理する必要なく、スケーリングも自動で行ってくれるも便利ですね。

API Gateway

API管理はAPI Gatewayを利用します。

Lambdaと直接やりとりせず、API Gatewayを挟んでリクエスト、レスポンス周りの処理を任せることで、Lambda側は機能の実装にフォーカスできます。

また、後述するCognitoとの連携もAPI Gatewayの層で実装できるため、バックエンドで複雑になりがちな認証/認可を見なくて済むのは、開発していく上でかなり助かりました。

Cognito

認証/認可及びユーザー管理ではCognitoユーザープールを採用しました。

Cognitoはログインページも含めたユーザー管理に必要な機能を丸ごと提供してくれて、簡単にユーザー周りを実装できます。

また、API Gatewayと連携することで、ユーザーのロールに基づいた各APIの認可も簡単に構築することができます。

SAM

SAMとは Serverless Application Model の略で、前述したLambda、API Gateway、CognitoをIaCで構築できるツールです。

サーバレスのインフラ構築といった特定のユースケースに最適化しているIaCであり、同じCloudFormationと比較しても、よりシンプルに構築することができます。

今回はサーバレスモデルを採用しているため、SAMを採用しました。

DynamoDB

本サービスはチャット形式のため、レコード数が多くなる傾向にあり、テーブルのサイズやスループットを動的にスケーリングしてくれるDynamoDBを採用しました。

料金も使用しているサイズに応じて発生するため、無駄なコストを出さないという点でも、個人開発のニーズに合っていました。

また、GSIを利用して、ユースケースごとのクエリを最適化できるのも、レコード数が多いチャットサービスには適しています。

サードパーティ

本サービスではチャットのレスポンス等の生成でOpenAI API、文章の英訳・和訳でAmazonTranslateを利用します。

AmazonTranslateは、呼び出し側のLambdaにIAMロールが付与されていれば、特にアクセスキーを用意せずにを利用できるため、AmazonTranslateを採用しました。


フロントエンドアプリ

React(Viteベース)

フロントエンドのフレームワークの選定ですが、本サービスは個人開発ということもあり、ベースとなる思想として、「費用を極力抑えたい」という思いがありました。

そのため、サーバサイドでレンダリングさせず、静的ファイルを配信する方針にしました。

また、今回はチャットページとログインページのみで、チャットページは都度生成される ID をパスにした Dynamic Route となります。

そのため、SSGにするメリットもほぼないため、従来のSPAでのビルドにしました。

SPAのビルドツールとしては、ビルドの速度が速く開発体験の良い Vite を採用しました。

JoyUI

デザインコストを下げるため、コンポーネントライブラリを利用しています。
使い慣れたMUIベースかつ柔軟性の高いJoyUIを採用しました。

Tailwind

CSSライブラリはTailwindを利用しています。

個人的な好みもあるかもしれませんが、要素とStyleを同じ場所に書けて、クラス名を命名する手間を省ける気軽さから、Tailwindを選びました。

ディレクトリ構成

フロントエンドのディレクトリ構成は以下のようになっています。

src
├── api/             // バックエンドとの通信を行う層
├── domain/          // フロントエンド内のドメイン。今回はClassは利用せず、Objectの型定義及びenum型の定数を利用  
├── hook/            // カスタマイズしたhook(主にAPIリクエスト周り)を利用
├── logic/           // アプリケーション内の処理を格納。主にデータ加工やapi層との橋渡しで利用
├── router/          // react-router-domのルーティング定義を格納
├── store/           // contextを格納。今回はAuth周りの管理で利用
└── view             // Atomic Design* 形式でコンポーネントを切り分け格納
    ├── atoms/       // ドメインを持たない最小単位のコンポーネント
    ├── molecules/   // ドメインを持たないatoms複数で構成されるコンポーネント
    ├── organizm/    // ドメインを持つコンポーネント
    ├── templage/    // page配下に配置。ページ全体のレイアウトをスタイリング
    └── page/        // ルーティング指定先、templateのラッパーとして利用
・
・
・

*Atomic Designの詳細はこちら

ディレクトリ構成で意識したことは、描画、hooks、ロジック、APIと各層ごと責務を分けたことです。
アプリの規模感やユースケースによって最適な構成も変わるので、ここは毎回悩みます…

各層の依存関係イメージ


バックエンドアプリ

Node.js

バックエンドの言語はNode.js(TypeScriptベース)を採用しました。

採用した主な理由は、個人開発とのこともあり、フロントエンドと同じ言語を利用して、開発速度を上げたかったからです。

また、バックエンドは保守性も考えて、後述するクリーンアーキテクチャで設計したかったため、型定義の手軽さという観点でもTypeSriptを利用しました。 

ディレクトリ構成

バックエンドのディレクトリ構成は、レイヤードアーキテクチャベースとしました。

function
├── application      // アプリケーションのユースケース
    └── usecase/     // 特定のビジネスケースを処理するためのロジック 
├── domain           // ビジネスルールやエンティティ
    ├── model/       // アプリケーションの状態を表すモデル
    ├── service/     // ドメイン内で利用するロジック(今回はpromptの中身など)
    └── type/        // ドメイン内で利用する型定義
├── infrastructure/  // 外部システムとの通信、ルーティング
    ├── adapter/     // サードパーティAPI(OpenAI,AmazonTranslate)との通信
    ├── registory/   // DBとの通信
    └── router.ts    // ルーティングを定義
└── interface/       // ユーザーや外部システムとのインターフェイス
    ├── controller/  // 外部からのリクエストを適切なユースケースに渡し、適切な形式でレスポンスする順序を定義 
    ├── request/     // 外部からのリクエストの中身を適切な形に変換
    └── response/    // アウトプットを適切なレスポンスの形に変換
・
・
・

以下の図の通り、内側の層が外側の層の抽象クラスを参照し、実際のクラスを外側から内側に与えることで(依存性の注入)、バックエンド全体を疎結合にする設計を意識しました。

よく見かけるThe Clear Architectureの図
DI(依存性の注入)


感想

本業をこなしつつ、サービス構想から開発完了まで約1ヶ月とタイトなスケジュールでしたが、今回のサービスを作る上で意識した「コストを最大限抑えて、実用的なサービスを提供」という観点は、ある程度達成できたのかなと思います!

生成AIへの知見がそこまでなかったので、ドキュメントが充実しているという観点でOpenAIを採用しましたが、もっとコスパが良いライブラリがあったかもしれません、、、
もし知っている人いたら教えてください!

ここまで読んでいただきありがとうございました!

この記事が気に入ったらサポートをしてみませんか?