🫠
弊社の「意識チョット低いアーキテクチャ」10選
「CTOの視点で選ぶ「最適な」アーキテクチャとは?」というイベントで登壇しました。
本記事は登壇資料をMarkdownとしてそのまま記事化したものです。スライドのほうが読みやすい方は、Speaker Deckで御覧ください!
自己紹介1|職歴、趣味など
- 職種・SNS
- 株式会社NoSchool CTO
- 2016年〜Webエンジニア。2019年〜現職
- Twitter(X): 名人|マナリンクCTO
- Zenn: https://zenn.dev/meijin
- 好きなHTTPヘッダーはCache-Control
- 趣味
- 将棋☗、カメラ📸、ラム酒🥃、個人開発💻、筋トレ💪、高校野球観戦⚾
自己紹介2|外部発信・諸活動
- ZennでReact記事が人気
- 歴代記事でLike数1位(登壇時点)
- 個人開発
- テストメーカー(ユーザー20,000人以上)
- エンジニア向け教材執筆
- 「LaravelでFat Controllerをリファクタしよう」
テーマ選定
事業紹介
- オンライン家庭教師マナリンク(https://manalink.jp)
- 普通の家庭教師会社と違って、先生を指名できるのが大きな特長
サービス特長・会社・組織規模
- マナリンクの技術的な特徴
- 特性の異なるWebサイトをマナリンクが包含
- 家庭教師を探せる検索サイト(toC向けメディアっぽい)
- 家庭教師が授業の予定、結果、売上などを管理できるサイト(toB向けSaaSっぽい)
- メディアとSaaSそれぞれ両立したほどよい最適化が宿命
- 特性の異なるWebサイトをマナリンクが包含
- 組織・会社
- 2020年のコロナ禍からサービス開始
- エンジニアは5名(CTO含む)、全員正社員、出社制
- 全員フルスタック
本日の内容
弊社の「意識チョット低いアーキテクチャ」を発表
-
意識低いのニュアンス例
- データorサービス規模が膨大になると耐えられない技術選定だけど今は耐え
- ルールとしての完成度は低いけど、各メンバーが自律的に動く前提で緩いルールで耐え
- 流行っている技術だけど人柱にはなりたくないので静観している
- 完璧を目指して各開発者が萎縮してほしくないので、緩くていいよ!と豪語している
💰完璧じゃない仕組みでも、何もやらないよりはマシでしょ!みたいな話
さっそく10選を紹介!
以後、バックエンドは【BE】、フロントエンドは【FE】と表記します
- 【BE】 定期バッチや遅延実行はFargateのコンテナをStep Functionsから呼んで済まそう
- 【BE】 とりあえずControllerからUseCaseは最悪切ろうね
- 【BE】 PHPだけどPHP Stanのレベルを10段階の6までにしてるよ
- 【BE】 Laravelの機能のうち、ORMにあまりにベタベタな機能はそもそも使わないでね
- 【FE】 Next.jsだけどApp Routerじゃないよ
- 【FE】 デザインシステムなんて整っていないけどTailwind CSSだよ
- 【FE】 WebもアプリもReactだけど、言うほどコード共通化してないよ
- 【BE/FE】 OpenAPI書かなくていいよ。aspidaでサクッと型付けしちゃおう
- 【BE/FE】 高速化がどうしてもしたいページはCloudFrontのSWRキャッシュで済ませるよ
- 【BE/FE】 腐敗防止層は適当でもめっちゃ価値あるから積極的に切ろう
前提知識
- マナリンクでは、バックエンド(Laravel)アプリケーションをAWS Fargateで運用
- マナリンクにおける定期バッチ処理
- Userへの各種通知や、統計データの生成など
- バッチ処理のためだけに別Repositoryを切ったり、別言語を使ったり、ドメインロジックを再実装したくない
AWS Step Functionsからアプリケーションと同じECS Taskを起動
Step Functionsについて軽く解説
- AWS Step Functions(Sfn)は多くのAWSサービスをフローチャートの要領で連携できるサービス
- トリガーにEventBridgeを設定でき、Cron式で定期実行が可能。タイムアウトやリトライも設定可
- SfnはECS Task起動時にStdInを指定できるので、定期バッチ名や引数も渡すことができる
以下の例ではJST17時にphp artisan command:introduce-trial-support
を
アプリケーションと同じDockerベースのインスタンスで実行する
IntroduceTrialSupport:
Type: Schedule
Properties:
Input: '{"commands": ["php", "artisan", "command:introduce-trial-support"]}'
Schedule: 'cron(0 8 * * ? *)'
メリット・意識チョット低いポイント
- メリット
- アプリケーションと同じドメインロジック・CI・ロギング等が再利用でき、効率的
- アプリケーションと同じインスタンスでは実行されないため、負荷の高い処理も割と安全
- AWS SAMを使いYamlベースで実行時刻の設定ができるため、Git管理しやすい
-
意識チョット低いポイント
- バッチの同時実行制御など、処理間をまたいだ制御がやりにくい
- 言語&FWがLaravel&PHP縛りになる
- 起動条件がCronで表現できないくらい複雑だと対応できない
- 1時間以上といった長時間実行/同時に数十台バッチが立ち上がることなどは想定していない
ControllerからUseCaseは最悪切る
- 最も最低のアーキテクチャラインを明言
- ControllerからUseCaseは最悪切る
- UseCase(以上)の単位の結合テストは必ず書く
- ※DDDで設計していたり、CQRSを採用する機能群もある
- 最悪のラインがそこな理由(意識が低いポイント!)
- バックエンド処理がWeb Front、ネイティブアプリ、社内向け管理画面、定期バッチ経由等から呼ばれるため、Controllerにベタ書きするとクライアントが増えたときに無駄に困る
- 実際Webサービス全部見ていると、DDDやCQRSがtoo muchな機能群も多い
- UseCase単位の結合テストさえあれば、後から肥大して設計頑張りたくなったときに安心
PHPだけどPHP Stanのレベルを6までにしてるよ
- PHPStanとは
- JavaScriptでいうESLintのような静的解析ツール
- PHP Docsの内容も考慮し、PHP自身よりリッチな型チェックが可能
- PHPStanのレベル
- 導入時、1から順に上げていったが、5のあたりで厳しすぎて開発効率が落ちてきた
- 現在は6にLaravel等に由来するIgnoreを多少加えて運用
level: 6
~中略~
ignoreErrors:
-
message: "#^Call to an undefined method Illuminate\\\\Support\\\\Collection\\:\\:.*\\(\\)\\.$#"
path: '**/*.php'
LaravelのORMベタベタ機能は使わない
- 我々が使わないORMベタベタ機能例
- Policy(認可処理。ORMベースで書き込み制御などを行う)
- Route Model Binding(Controllerの引数に操作対象のORMインスタンスが注入)
- API Resource(完全にベタベタではないが、ORM依存させると超便利になる)
// 引数にORMが渡ってくるのがRoute Model Binding、返り値がAPI Resource
public function edit(\App\Models\User $user, string $email)
{
$user->email = $email;
$user->save();
$user->refresh();
return new UserResource($user);
}
ORMベタベタ機能を使わない理由
- 一言でいうと
- テーブル構造が依存する範囲が無駄に広がるから
- かつ、弊社のアプリケーション特性上、それが辛いことが多いから
- テーブル構造の依存が広がると困ること
- 同じデータを、機能ごとに別のものとして解釈することが困難になる
- たとえば「先生の指導コース」は生徒にとっては授業内容の明示、先生にとっては収入源、マナリンクにとってはCtoCのトラブルを防ぐための契約条件明示
- 機能群によって欲しいデータ、紐づくデータ、Mutationする内容などが異なる
- これが往々にしてあとから発覚するのがアプリ開発あるある
- 同じデータを、機能ごとに別のものとして解釈することが困難になる
Next.jsだけどApp Routerじゃないよ
- 2024年10月現在、弊社ではまだPages Routerを利用
- 思考プロセス
- 個人開発でApp Routerを検証し、ざっくり把握&移行見立ては立てている
-
'use client'
しまくれば(理想かはさておき)移行自体は最悪できそう
-
-
revalidateTag
が動かないIssueが上がるなど、まだBuggyな部分がある&キャッシュのOpt-inなど思想も不安定 - キャッシュ戦略はCloudFrontで今は十分(後述)だし、メディア&SaaSの特性が両方持っていてベストプラクティスが決めにくい
- ➡️前向きに移行するより必須度が上がったら移行する感じ
- 個人開発でApp Routerを検証し、ざっくり把握&移行見立ては立てている
デザインシステムなんて整っていないけどTailwind CSSだよ
- デザインシステムが整っていなくてもTailwind CSSは嬉しいよ
- 今後App Router移行するときにUI層の懸念が小さめ
- ⬇️たとえばz-indexの管理を意識低い感じでできる
// tailwind.config.js
zIndex: {
modal: 'var(--z-index-modal)',
header: 'var(--z-index-header)',
menu: 'var(--z-index-menu)',
},
// src/index.css
--z-index-modal: 110;
--z-index-header: 105;
--z-index-menu: 100;
WebもアプリもReactだけど、言うほどコード共通化してないよ
- ReactとReact Nativeは、別物
- そもそもDOMが異なる
- 要件レイヤーで見ても、Webとアプリが常に一致とは限らない
- ”何”が共通化できている?
- aspida(次節)/SWRを使ったAPI型定義や状態管理など、基底となる考え方
- CompositionやHooksの切り方といった設計パターンの共通化
- (失敗談)同機能をWeb/Appでアサイン割りして事故った。機能ごとにアサインがベスト
OpenAPI書かなくていいよ。aspidaでサクッと型付けしちゃおう
- aspidaとは
- APIの型定義をTSで書くと、型安全にAPIを叩くクライアントが自動生成されるライブラリ
export interface Methods {
get: {
resBody: {
is_available: boolean;
};
};
}
const { data, error, isValidating, mutate: revalidate } = useAspidaSWR(apiClient.available);
console.log(data.is_available); // boolean type safe
OpenAPIを書かない理由
- 工数がかかる上にメリットが小さい
- 全メンバーがフルスタックのため、FE/BE境界のドキュメント化必須度が低い
- Mutate寄りのAPIが多く、表面上のDocsよりDomain層のコード読むほうが速い
- 欲しい理由があるとしたら
- APIの設計レビューの統一Formatとして扱うのは魅力的かも
※aspida自体はOpenAPIとのCodegen対応しています。弊社がチョット意識低いだけです
高速化がどうしてもしたいページはCloudFrontのSWRキャッシュで済ませるよ
- 再掲)マナリンクでは前段にCloudFrontを置いていて、かつAWS CDKで設定を管理している
高速化に関しての基本的なスタンス
- 私(CTO)は割とパフォーマンス頑張りたい人
- FEでは)Lighthouseを見て凡ミスは全部倒す、CLSできるだけ0、画像最適化もimgixを使うなどして低コストで実現
- BEでは)DDDは好きとはいえSQLを叩きまくる富豪的なスタンスは好きではなく、基本はN+1も許容しないし、クエリ回数をCIで監視している
- キャッシュは思わぬエンバグのリスクが高いのでできるだけ避けたい
- メディア側の画面(先生一覧など)の高速化要件は
- CloudFront × Cache-Control w/ stale-while-revalidateでキャッシュする
- 動的ページでも、古いキャッシュが割とすぐ揮発するので苦情になりにくい👍
CloudFrontのSWRキャッシュの設定例
- Next.js(Pages Router)での設定
- 正直、Next.js内部のキャッシュに頼るより一般的なProtocolに則るのでわかりやすい
- キャッシュであまり苦労したくないから、基本CloudFrontキャッシュだけで頑張るよ!が
意識チョット低い
export function setCacheControlForSSRPage(
context: GetServerSidePropsContext<ParsedUrlQuery, string | false | object | undefined>,
) {
context.res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=86400');
}
腐敗防止層は適当でもめっちゃ価値あるから積極的に切ろう
- 腐敗防止層とは
- あるモジュールがN箇所に依存していると、シグネチャの変更時にN箇所に影響が伝播するが、間にシグネチャを吸収するモジュールを挟むことで、変更時の影響を1箇所のみに絞る
- ただ切るだけじゃなくて、引数の型やモジュール名を一段階抽象的にすることが望ましい
➡️次スライドで「史上最強に意識が低い腐敗防止層」を紹介
史上最強に意識が低い腐敗防止層
import { Tab as LibTab } from '@headlessui/react';
export const Tab = LibTab;
export const TabPanel = LibTab.Panel;
export const TabList = LibTab.List;
export const TabPanels = LibTab.Panels;
export const TabGroups = LibTab.Group;
- ...こんなただLibraryからImportしてExportするだけのコンポーネント、意味があると思いますか?
あります!
import { Tab as LibTab } from '@headlessui/react';
export const Tab = LibTab;
export const TabPanel = LibTab.Panel;
export const TabList = LibTab.List;
export const TabPanels = LibTab.Panels;
export const TabGroups = LibTab.Group;
- 背景知識
- Headless UIは、特定バージョンから
Tab.Panels
コンポーネントをTabPanels
に変更した - 現バージョンのHeadless UIから直接TabをImportしていると、ライブラリアプデ時に、
N箇所のコンポーネントをTab.Panels
からTabPanels
に変更する必要がある - あらかじめ内製Wrapperで
Tab.Panels
をTabPanels
に変換しておくことで、アプデ時の影響範囲を1箇所に絞ることができる
- Headless UIは、特定バージョンから
【まとめ】意識が低いアーキテクチャに価値はあるか?
- 小さなアーキテクチャなくして、大きなアーキテクチャなし
- よく”大規模サービスやりたい”といった志向を見聞きしますが、個人的には
小さなアーキテクチャをしっかり作りきれることがまず大事 - 大きなアーキテクチャは、小さなアーキテクチャをN回積み重ねることで作られる
- よく”大規模サービスやりたい”といった志向を見聞きしますが、個人的には
- 完璧じゃない改善でも、やらないよりはマシ!
- 「単にTabのWrapperを作るよりもっとDOM構造ごとWrapperにできないかな」など、
つい欲張ってしまうが、欲張りすぎて何もできなかったら本末転倒 - 意識低い改善で終わらせるには意外と勇気がいるが、やりきろう
- 「単にTabのWrapperを作るよりもっとDOM構造ごとWrapperにできないかな」など、
ご清聴ありがとうございました!
follow me🐦: @meijin_garden
オンライン家庭教師マナリンクを運営するスタートアップNoSchoolのテックブログです。 manalink.jp/ 創業以来年次200%前後で売上成長しつつ、技術面・組織面での課題に日々向き合っています。 カジュアル面談はこちら! forms.gle/fGAk3vDqKv4Dg2MN7
Discussion