Slide 1

Slide 1 text

弊社の 「意識チョット低いアーキテクチャ」 10選 株式会社NoSchool CTO / meijin

Slide 2

Slide 2 text

アジェンダ 自己紹介・事業紹介 意識低いアーキテクチャ10選 まとめ

Slide 3

Slide 3 text

自己紹介1|職歴、趣味など 職種・SNS 株式会社NoSchool CTO 2016年〜Webエンジニア。2019年〜現職 Twitter(X): 名人|マナリンクCTO Zenn: https://zenn.dev/meijin 好きなHTTPヘッダーはCache-Control 趣味 将棋☗、カメラ 📸、ラム酒 🥃、個人開発 💻、筋トレ 💪、高校野球観戦 ⚾

Slide 4

Slide 4 text

自己紹介2|外部発信・諸活動 ZennでReact記事が人気 歴代記事でLike数1位(登壇時点) 個人開発 テストメーカー(ユーザー20,000人以上) エンジニア向け教材執筆 「LaravelでFat Controllerをリファクタしよう」

Slide 5

Slide 5 text

テーマ選定

Slide 6

Slide 6 text

事業紹介 オンライン家庭教師マナリンク(https://manalink.jp) 普通の家庭教師会社と違って、先生を指名できるのが大きな特長

Slide 7

Slide 7 text

サービス特長・会社・組織規模 マナリンクの技術的な特徴 特性の異なるWebサイトをマナリンクが包含 家庭教師を探せる検索サイト(toC向けメディアっぽい) 家庭教師が授業の予定、結果、売上などを管理できるサイト(toB向けSaaSっぽい) メディアとSaaSそれぞれ両立したほどよい最適化が宿命 組織・会社 2020年のコロナ禍からサービス開始 エンジニアは5名(CTO含む)、全員正社員、出社制 全員フルスタック

Slide 8

Slide 8 text

本日の内容

Slide 9

Slide 9 text

弊社の「意識チョット低いアーキテクチャ」を発表 意識低いのニュアンス例 データorサービス規模が膨大になると耐えられない技術選定だけど今は耐え ルールとしての完成度は低いけど、各メンバーが自律的に動く前提で緩いルールで耐え 流行っている技術だけど人柱にはなりたくないので静観している 完璧を目指して各開発者が萎縮してほしくないので、緩くていいよ!と豪語している 💰完璧じゃない仕組みでも、何もやらないよりはマシでしょ!みたいな話

Slide 10

Slide 10 text

さっそく10選を紹介! 以後、バックエンドは【BE】 、フロントエンドは【FE】と表記 します

Slide 11

Slide 11 text

【BE】定期バッチや遅延実行はFargateのコンテナをStep Functionsから呼んで済まそう 1 【BE】とりあえずControllerからUseCaseは最悪切ろうね 2 【BE】PHPだけどPHP Stanのレベルを10段階の6までにしてるよ 3 【BE】Laravelの機能のうち、ORMにあまりにベタベタな機能はそもそも使わないでね 4 【FE】Next.jsだけどApp Routerじゃないよ 5 【FE】デザインシステムなんて整っていないけどTailwind CSSだよ 6 【FE】WebもアプリもReactだけど、言うほどコード共通化してないよ 7 【BE/FE】OpenAPI書かなくていいよ。aspidaでサクッと型付けしちゃおう 8 【BE/FE】高速化がどうしてもしたいページはCloudFrontのSWRキャッシュで済ませるよ 9 【BE/FE】腐敗防止層は適当でもめっちゃ価値あるから積極的に切ろう 10

Slide 12

Slide 12 text

①【BE】 定期バッチや遅延実行は Fargateのコンテナを Step Functionsから呼んで済まそう

Slide 13

Slide 13 text

前提知識 マナリンクでは、バックエンド(Laravel)アプリケーションをAWS Fargateで運用 Welcome to PlantUML! You can start with a simple UML Diagram like: Bob->Alice: Hello Or class Example You will find more information about PlantUML syntax on https://plantuml.com (Details by typing license keyword) PlantUML 1.2024.8beta3 [From string (line 3) ] @startuml !define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v18.0/dist !include AWSPuml/AWSCommon.puml Cannot open URL

Slide 14

Slide 14 text

AWS Step Functionsからアプリケーションと同じ ECS Taskを起動 Welcome to PlantUML! You can start with a simple UML Diagram like: Bob->Alice: Hello Or class Example You will find more information about PlantUML syntax on https://plantuml.com (Details by typing license keyword) PlantUML 1.2024.8beta3 [From string (line 3) ] @startuml !define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v18.0/dist !include AWSPuml/AWSCommon.puml Cannot open URL

Slide 15

Slide 15 text

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 * * ? *)'

Slide 16

Slide 16 text

メリット・意識チョット低いポイント メリット アプリケーションと同じドメインロジック・CI・ロギング等が再利用でき、効率的 アプリケーションと同じインスタンスでは実行されないため、負荷の高い処理も割と安全 AWS SAMを使いYamlベースで実行時刻の設定ができるため、Git管理しやすい 意識チョット低いポイント バッチの同時実行制御など、処理間をまたいだ制御がやりにくい 言語&FWがLaravel&PHP縛りになる 起動条件がCronで表現できないくらい複雑だと対応できない 1時間以上といった長時間実行/同時に数十台バッチが立ち上がることなどは想定していない

Slide 17

Slide 17 text

ここまで書いて気がついたのですが、このペースで10個解説す ると日が暮れそう(日が昇りそう?)なので ここからガチで意識低めでいきます! 🏃🏃🏃🏃🏃🏃

Slide 18

Slide 18 text

②【BE】 とりあえずControllerから UseCaseは最悪切ろうね

Slide 19

Slide 19 text

ControllerからUseCaseは最悪切る 最も最低のアーキテクチャラインを明言 ControllerからUseCaseは最悪切る UseCase(以上)の単位の結合テストは必ず書く ※DDDで設計していたり、CQRSを採用する機能群もある 最悪のラインがそこな理由(意識が低いポイント!) バックエンド処理がWeb Front、ネイティブアプリ、社内向け管理画面、定期バッチ経由等 から呼ばれるため、Controllerにベタ書きするとクライアントが増えたときに無駄に困る 実際Webサービス全部見ていると、DDDやCQRSがtoo muchな機能群も多い UseCase単位の結合テストさえあれば、後から肥大して設計頑張りたくなったときに安心

Slide 20

Slide 20 text

③【BE】 PHPだけどPHP Stanの レベルを10段階の6までにしてるよ

Slide 21

Slide 21 text

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'

Slide 22

Slide 22 text

④【BE】 Laravelの機能のうち、 ORMにあまりにベタベタな機能は そもそも使わないでね

Slide 23

Slide 23 text

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); }

Slide 24

Slide 24 text

ORMベタベタ機能を使わない理由 一言でいうと テーブル構造が依存する範囲が無駄に広がるから かつ、弊社のアプリケーション特性上、それが辛いことが多いから テーブル構造の依存が広がると困ること 同じデータを、機能ごとに別のものとして解釈することが困難になる たとえば「先生の指導コース」は生徒にとっては授業内容の明示、先生にとっては収 入源、マナリンクにとってはCtoCのトラブルを防ぐための契約条件明示 機能群によって欲しいデータ、紐づくデータ、Mutationする内容などが異なる これが往々にしてあとから発覚するのがアプリ開発あるある

Slide 25

Slide 25 text

⑤【FE】 Next.jsだけどApp Routerじゃない よ

Slide 26

Slide 26 text

Next.jsだけどApp Routerじゃないよ 2024年10月現在、弊社ではまだPages Routerを利用 思考プロセス 個人開発でApp Routerを検証し、ざっくり把握&移行見立ては立てている 'use client' しまくれば(理想かはさておき)移行自体は最悪できそう revalidateTag が動かないIssueが上がるなど、まだBuggyな部分がある&キャッシュの Opt-inなど思想も不安定 キャッシュ戦略はCloudFrontで今は十分(後述)だし、メディア&SaaSの特性が両方持っ ていてベストプラクティスが決めにくい ➡️前向きに移行するより必須度が上がったら移行する感じ

Slide 27

Slide 27 text

⑥【FE】 デザインシステムなんて整っていな いけど Tailwind CSSだよ

Slide 28

Slide 28 text

デザインシステムなんて整っていないけど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;

Slide 29

Slide 29 text

⑦【FE】 WebもアプリもReactだけど、 言うほどコード共通化してないよ

Slide 30

Slide 30 text

WebもアプリもReactだけど、言うほどコード共通化 してないよ ReactとReact Nativeは、別物 そもそもDOMが異なる 要件レイヤーで見ても、Webとアプリが常に一致とは限らない ”何”が共通化できている? aspida(次節)/SWRを使ったAPI型定義や状態管理など、基底となる考え方 CompositionやHooksの切り方といった設計パターンの共通化 (失敗談)同機能をWeb/Appでアサイン割りして事故った。機能ごとにアサインがベスト

Slide 31

Slide 31 text

⑧【BE/FE】 OpenAPI書かなくていいよ。 aspidaでサクッと型付けしちゃおう

Slide 32

Slide 32 text

OpenAPI書かなくていいよ。aspidaでサクッと型付 けしちゃおう aspidaとは APIの型定義をTSで書くと、型安全にAPIを叩くクライアントが自動生成されるライブラリ const { data, error, isValidating, mutate: revalidate } = useAspidaSWR(apiClient.available); console.log(data.is_available); // boolean type safe export interface Methods { get: { resBody: { is_available: boolean; }; }; }

Slide 33

Slide 33 text

OpenAPIを書かない理由 工数がかかる上にメリットが小さい 全メンバーがフルスタックのため、FE/BE境界のドキュメント化必須度が低い Mutate寄りのAPIが多く、表面上のDocsよりDomain層のコード読むほうが速い 欲しい理由があるとしたら APIの設計レビューの統一Formatとして扱うのは魅力的かも ※aspida自体はOpenAPIとのCodegen対応しています。弊社がチョット意識低いだけです

Slide 34

Slide 34 text

⑨【BE/FE】 高速化がどうしてもしたいページは CloudFrontのSWRキャッシュで済 ませるよ

Slide 35

Slide 35 text

高速化がどうしてもしたいページはCloudFrontの SWRキャッシュで済ませるよ 再掲)マナリンクでは前段にCloudFrontを置いていて、かつAWS CDKで設定を管理している

Slide 36

Slide 36 text

高速化に関しての基本的なスタンス 私(CTO)は割とパフォーマンス頑張りたい人 FEでは)Lighthouseを見て凡ミスは全部倒す、CLSできるだけ0、画像最適化もimgixを使 うなどして低コストで実現 BEでは)DDDは好きとはいえSQLを叩きまくる富豪的なスタンスは好きではなく、基本は N+1も許容しないし、クエリ回数をCIで監視している キャッシュは思わぬエンバグのリスクが高いのでできるだけ避けたい メディア側の画面(先生一覧など)の高速化要件は CloudFront × Cache-Control w/ stale-while-revalidateでキャッシュする 動的ページでも、古いキャッシュが割とすぐ揮発するので苦情になりにくい 👍

Slide 37

Slide 37 text

CloudFrontのSWRキャッシュの設定例 Next.js(Pages Router)での設定 正直、Next.js内部のキャッシュに頼るより一般的なProtocolに則るのでわかりやすい キャッシュであまり苦労したくないから、基本CloudFrontキャッシュだけで頑張るよ!が 意識チョット低い export function setCacheControlForSSRPage( context: GetServerSidePropsContext, ) { context.res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=86400'); }

Slide 38

Slide 38 text

⑩【BE/FE】 腐敗防止層は適当でもめっちゃ価値 あるから積極的に切ろう

Slide 39

Slide 39 text

腐敗防止層は適当でもめっちゃ価値あるから積極的 に切ろう 腐敗防止層とは あるモジュールがN箇所に依存していると、シグネチャの変更時にN箇所に影響が伝播する が、間にシグネチャを吸収するモジュールを挟むことで、変更時の影響を1箇所のみに絞る ただ切るだけじゃなくて、引数の型やモジュール名を一段階抽象的にすることが望ましい ➡️次スライドで「史上最強に意識が低い腐敗防止層」を紹介

Slide 40

Slide 40 text

史上最強に意識が低い腐敗防止層 …こんなただ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;

Slide 41

Slide 41 text

あります! 背景知識 Headless UIは、特定バージョンから Tab.Panels コンポーネントを TabPanels に変更した 現バージョンのHeadless UIから直接TabをImportしていると、ライブラリアプデ時に、 N箇所のコンポーネントを Tab.Panels から TabPanels に変更する必要がある あらかじめ内製Wrapperで Tab.Panels を TabPanels に変換しておくことで、アプデ時の 影響範囲を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;

Slide 42

Slide 42 text

【まとめ】 意識が低いアーキテクチャに 価値はあるか?

Slide 43

Slide 43 text

【まとめ】意識が低いアーキテクチャに価値はある か? 小さなアーキテクチャなくして、大きなアーキテクチャなし よく”大規模サービスやりたい”といった志向を見聞きしますが、個人的には 小さなアーキテクチャをしっかり作りきれることがまず大事 大きなアーキテクチャは、小さなアーキテクチャをN回積み重ねることで作られる 完璧じゃない改善でも、やらないよりはマシ! 「単にTabのWrapperを作るよりもっとDOM構造ごとWrapperにできないかな」など、 つい欲張ってしまうが、欲張りすぎて何もできなかったら本末転倒 意識低い改善で終わらせるには意外と勇気がいるが、やりきろう

Slide 44

Slide 44 text

ご清聴ありがとうございました! follow me 🐦: @meijin_garden 📣懇親会で話したいこと 📣 「私の会社の意識チョット低いアーキテクチャ」 「最近やった、小さな改善」 「リファクタしたい、改善したいといった要望を形にするまでのフロー」