2024.10.21
お互い疑心暗鬼になりがちな、経営企画と事業部の壁 組織に「分断」が生まれる要因と打開策
真っ当な技術を使ったふつうのWebサービス開発(全1記事)
リンクをコピー
記事をブックマーク
野崎翔太氏(以下、野崎):まず最初に自己紹介をさせて下さい。僕は今年の1月にログミーに入社した、社内で唯一のエンジニアです。そのため、サービスの技術的な所は分野に関わらず、すべて担当しています。技術的な部分以外ですと、サービスのWebデザインも担当しています。
Web上ではemonkakというハンドルで活動しています。直近ですとFeedponというLivedoor ReaderライクなRSSリーダーを開発して公開しています。あとは仕事でメインで使っていたのもあって、PHPのライブラリをいくつか公開していたりもしますが、PHPが好きという訳でないです。好きな言語はHaskellです。
続いて、ログミーのサービスについて簡単に説明させて下さい。ログミーは世界をログする書き起こしメディアということで、主にリアルで開催されたイベントの書き起こしを記事にして配信しています。
メディアの展開としましては、主としてIT、スタートアップ関係のイベントを書き起こすログミー本体の他に、エンジニア向け勉強会の書き起こしをするログミーTech、投資家向けに決算説明会の書き起こしをするログミーファイナンスがあります。
今回のセッションでお話しするのは、ログミー本体とログミーTechのリニューアルになります。ログミーファイナンスにつきまして、別システムでリニューアルをする予定で、現在開発中となっています。
その前に、まずはリニューアル以前のシステムについてどうだったのかお話します。
以前のシステムはWordPressで独自にテーマ、プラグインを開発して運用していました。この開発・運用、あとはデザインに関して社外のエンジニア・デザイナーの方にお願いしていました。
このWordPressのシステムに先程お話した、ログミー、ログミーTech、ログミーファイナンスの3つのメディアがすべて載っていた形になります。現在はログミーファインスについてのみ、このWordPressの旧システムで動作しています。
WordPressについてはプラグインやテーマが豊富で、手軽に扱えるCMSではありますが、みなさんご存知の通り、作りはレガシーで技術的には少々問題があります。そういったこともあって、ちょっとした機能追加も中々面倒で、注意しないとすぐにコードも負債化してしまい保守も難しくなります。
特に問題だったのが遅いという点で、ユーザーはページがなかなか表示されなくてストレスになりますし、編集部の業務効率も落ちます。実際にレスポンスを返すのに約2000msもの時間を要していた時期がありました。これはちょっとWebサービスとして有り得ないレベルですよね。
リニューアルを進めているとはいえ、それまでずっとこのままにしておくのは流石に許容できませんでした。これはキャッシュやテーマのチューニングで約400msまではなんとか改善させました。それでもまだ遅いんですけどね。
そしてリニューアルにということになるのですが、実は僕の入社以前からシステムのリニューアルプロジェクトは進行していました。しかし、このプロジェクトは失敗に終わってしまいました。というより僕の方からプロジェクトの終了を提案しました。僕の入社後最初の1ヶ月の仕事はこれですね。
このプロジェクトは、現行のWordPressのシステムから必要な機能だけに絞って、PHPのポピュラーなWebフレームワークであるLaravelで再実装するというものです。まずはリニューアルにあたって技術的な課題を解決することを優先したということですね。開発者主導でビジネスサイドの要望は後回し、とりあえずシステムを新しくすると。
しかし、ビジネスサイドの要望をヒアリングしてみると、当然ながら現行のシステムの再発明がしたいということではなかった訳です。それはこのリニューアルをそのまま進めたとして、すぐに大幅に設計を改めなければならないということを意味しました。
そこで体制も新たにプロジェクトをリスタートさせることにしました。
開発は完全に内製化、さらにドメイン駆動設計を導入し、編集部の業務をヒアリングし、ビジネスサイドの要望を受けながら特定の「技術」ではなく「ドメイン」駆動で開発を進めていくことにしました。
そこでまずは、ドメインエキスパートと開発者の共通言語たるユビキタス言語を定義して、そこからドメインモデルを構築しました。
ドメインモデルを構築するとは言っても、システム上で行うメディアの業務は記事を作成してそれを公開するといった単純なものです。実際に悩む所は少なかったのですが、ログミー独自の概念「シリーズ」というものがありまして、これの扱いに関しては苦労する所がありました。
「シリーズ」の言葉通り日本語で言う「連載」という続きもの記事の集合であれば話は単純だったのですが、それは正確な理解ではありませんでした。
なんと「シリーズ」は親子関係を持つことができて、この時点で単純に続きものの記事の集合とは言えない訳です。
しかし何らかの関連性は持っていると。これを理解する鍵がこの親子関係が最大3代までしかないことにありました。
実は存在するすべてのシリーズが3代ある訳ではないことが理解を妨げていたのですが、シリーズのそれぞれの代は役割が異なるものだったのです。別々の概念をシリーズという言葉でまとめていて、それを親子関係にしていたわけです。
当初、このことはドメインエキスパートたる編集部の人間でさえ体系的には理解していなくて、ディスカッションを重ねてドメイン知識を蒸留することで得ることのできた新しい知識です。
そして、これら三代のシリーズは、上の代からそれぞれ、イベントの主催者たる「コミュニティ」、そのコミュニティによって開催された「イベント」、イベントで行なわれたセッションを記録した記事の集合たる「ログ」というように、明確に定義することができました。
最後に余談ですが、seriesは単数形と複数形の区別がない単複同形の名詞なのでコード上で使うとちょっとわかりにくい単語ですね。
さて、ここまででドメインモデルの構築まで終わりました。ここから先は技術の話です。
まずサービスをリニューアルするということは、現状のシステムに何らかの課題があるというのと、そのサービスの売り上げが立っている訳ですよね。売れていないサービスをリニューアルすることは普通ないはずで、やるとしたらコストをなるべく掛けずに部分的にちょっとずつ直すという感じかと思います。また、売り上げが立っているということは、サービスの継続可能性が高い訳で、長期間の保守に耐えられる設計が必要です。
逆に売れるかわからない新規のサービスは、保守性よりもコストを掛けずにいかに早く作るのかがより重要になります。とはいえ、保守性を犠牲していいという訳ではありませんけどね。
このセッションのタイトルにもありますように、今回のリニューアルに関しては真っ当な技術を選択するという方針で技術を選択しました。「真っ当な」というのは、標準化・規格化された技術、あるいはデファクトスタンダードの技術を示す意味で使っています。
これらの技術は長期間のサポートが期待されますし、ある規格に準拠した複数の実装があったとして、その間の移行も容易です。今使っている実装が古くなって、新しい良い実装が出てきたら、それを捨てて乗り換えることができる訳です。
一方で枯れた技術という言葉もあります。言い換えるなら成熟して安定しているので、更新をほとんど必要としない技術でしょうか。このような技術もまた長期間の保守という観点からも望ましいです。
しかし、有用な枯れた技術というのはあまりないのではないかと考えています。というのも、一定の複雑さを持った技術というのは、使われ続ける限り何だかんだ更新が発生するからです。更新が発生し続ける限りなかなか技術が成熟しない。つまりは枯れないということです。
また、外的要員の変化によって使われなくなった技術というのもあります。しかし、これを枯れた技術だと有り難がっても良いことはないですよね。
まず、アプリの実装にはPHPを採用しています。フロントにはnginxを置いてFastCGIでPHPとやり取りをするという、いたって「ふつう」の構成です。
何故PHPかといいますと、最初のリニューアルプロジェクトがPHPでLaravelを使って進んでいたので、そのままそれを踏襲した形ですね。個人的にPHPは好みませんが、動的型付け言語でありながら型注釈を付けてある程度は型安全なコードが書けるので、そう悪くはないと思います。何だかんだ利用者も多くライブラリも充実していますしね。システム要件としてもいたって「ふつう」のWebサービスなので、PHPで困ることも考えにくいです。
あとはデータベースにMySQL、キャッシュとセッションの管理にmemcached、ジョブスケジューラにcrondと、これも「ふつう」の構成ですね。
バックエンドのシステム全体としては、現状はモノリシックな構成となっています。ただ今後は、アクセスを集計して、ランキング・関連記事の取得などをするレコメンド処理を、マイクロサービスとして切り分けることを予定しています。
PHP使っている方はご存知だと思いますが、PHP-FIGという組織がありまして、ここがPSRという形で様々な仕様の標準化を勧告しています。Javaで言うところのJSRのようなものですね。この勧告はPHPコミュニティでかなりの影響力があって、数多くのライブラリ、フレームワークがこの勧告で規定されるインターフェイスを実装しています。
そのPSRの中にHTTPサーバーのインターフェイスを定義したPSR-15という勧告があります。これはリクエストを受け取ってレスポンスを返すというサーバーの動作を抽象化した、いたってシンプルなものです。
実際のインターフェイスはこのような定義です。
RequestHandlerInterface
というのがMVCスタイルのWebフレームワークで言う所のコントローラーに相当するものですね。リクエストを受け取ってレスポンスを返すと。MiddlewareInterface
についてはnode.jsのExpressだとか、最近のWebフレームワークに良くある仕組みで、渡ってきたリクエスト、あるいは生成されたレスポンスに対して何か途中で処理を挟むための仕組みですね。
あとはこのリクエストハンドラーとミドルウェアを実行する仕組みがあれば、リクエストを生成してレスポンスを返すという、HTTPサーバーの基本的な仕組みはできてしまう訳です。
今回のリニューアルではこのPSR-15を利用することで、特定のWebフレームワークを利用せずにアプリを実装することにしました。
ありもののWebフレームワーク、特にRailsなどのフルスタックのフレームワークは機能が豊富で便利ではありますが、更新の頻度が比較的多いです。そのため、長期の運用で最新バージョンに追従し続けるのもなかなか大変です。
プロトタイプを早く作るという場面では効果的ではありますが、今回は長期の運用がよりし易いシンプルな形にしたかったので、フレームワークレスという形を取りました。フレームワークレスと言っても、Serverlessが実際にはサーバーがあるように、フレームワークがない訳でないのですが、その存在をほぼ意識しないで開発することができるという意味ですね。
続いて、ルーティングに関してはトライ木を使った独自の仕組みをPSR-15のミドルウェアとして実装しています。これはパスをスラッシュ区切りでトライ木を構築してキャッシュ、そこから高速にルーティングできるというのが特徴となっています。
PSR-15の他には、DIコンテナのインターフェイスを定義しているPSR-11も利用しています。具体的にはPSR-15のリクエストハンドラーを実装したクラスにコンストラクターインジェクションする形ですね。DIコンテナの実装としては自前で開発したものを使っていまして、これはクラスの依存グラフを丸ごとキャッシュしてしまうことで、インスタンス化を高速に行えるのが特徴となっています。
PSR-15でHTTPサーバーの実装ができた訳ですが、Webサービスにはもう1つ重要な技術要素として永続化があります。
永続化に関してはJavaでいうJPAのようなものは残念ながらPSRには存在しません。ただいずれにせよ、JPAのようなものはちょっと複雑すぎるのでもっとシンプルなものが欲しいです。
しかし、その要望を満たすようなライブラリが無かったので、これもまた自前で開発しました。いわゆるData-MapperスタイルのORMで、マッピングされる対象のオブジェクトは永続化に関する振舞いのないプレーンなオブジェクトとしてして定義できます。
一方で、アプリケーション層、あるいはドメイン層が独自のライブラリに依存するのはある主のリスクです。その点、このORMに依存するのはRepositoryの実装のみで、Entityはプレーンなオブジェクトとして定義できます。このことはDDDと相性も良いです。
このORMを使ったリポジトリの実装はおおよそこのようになっています。
SQL文法を表すGrammarInterface
からクエリビルダーを作成して、クエリを構築。あとは getResult()
でArticleクラスにマッピングされたオブジェクトの結果セットを取得すると。最後に結果セットから firstOrDefault()
で最初の要素を取り出して、なかったらnullを返すということをしています。
結果セットはこのようにC#のLINQ相当の豊富な操作ができるようになっています。あとは、with()
メソッドでリレーションを与えることができて、一対一、一対多、多対多、ポリモーフィック関連などの関連を表現できます。
先程までの話はドメイン層のEntityの永続化に関するものでした。一方で、プレゼンテーションで標準するためにデータをどう表現するのかという問題もあります。ドメイン層のEntityとValue Objectをそのままプレゼンテーション用のモデルとして用いることができれば話は単純ですか、そうはいかない場合があります。ドメインモデルにプレゼンテーションの振る舞いが混ざってしまうのも良くありません。
そこでCQRSという更新と読み取りを分けて設計しましょうという考え方を利用します。更新に対応するコマンドモデルはドメインモデルをそのまま使い、読取に対応するクエリモデルをプレゼンテーションモデルとして新たにを作ります。プレゼンテーションモデルの実装に関しては、Getterだけを持った「○○Data」というクラスを、リポジトリに対応するものは「○○QueryService」のような名称でそれぞれアプリケーション層に作成しています。コマンドモデルがEntityとRepositoryで、クエリモデルがDataとQueryServiceといった形ですね。
次は、フロントエンドの構成についてお話します。
まず、JSのビルドツールについてはベーシックにWebpackのLoaderを通してBabelとTypeScriptを利用しています。今回はIE11対応も必要でしたのでPolyfillの注入だったり、ES2015文法のトランスパイルにBabelを利用しています。
フロントの作りとしてはSPAではなく各ページごとに独立しているMulti Page Applicationで、いわゆる伝統的なWebページですね。MPAなので当然ながらServer-Side Rendering等もないので要件としてはいたって「ふつう」です。
ただMPAの世界で意外と困るのが、JSのブートストラップをどこでやるのかという問題です。
例えばあるコンポーネントのhtmlがあったとして、このhtmlに対応するJSをどこで実行するかという話です。
jQueryの時代ですとこのようなコードがHTML内のscriptタグに直接書かれたり、ページごとにJSファイル作ってそれを読み込んでいましたね。しかし、この方法はコンポーネント単位での再利用性が難しくなりますし、JSのコードとhtmlの情報が離れてしまって管理も難しいです。
Reactなんかを使う場合でみMPAの場合は ReactDOM.render()
をどこに書くべきかとう問題が生じます。MPAだとページ全体をReactのコンポーネントにはしないので、そのページ内に配置されるすべてのコンポーネントについて ReactDOM.render()
をどこかで呼ばないといけません。
そこで考えたのがWebComponentsを使うという方法です。
WebComponentsモダンブラウザではネイティブサポートされていますし、非対応のIE11でもPolyfill読み込めば問題なく動作します。ブラウザの機能なのでpolyfill以外のライブラリが必要ないのもメリットです。
WebComponentsでカスタム要素を定義すれば、あとはタグを書くだけでJSのブートストラップは自動的やってくれます。これで、ブートスラップコードをどこに書くべきかという問題はそもそも存在しなくなった訳です。
あとはWebComponentsの機能そのままだとDOMツリーをひたすら手で触らないといけないので、ReactライクにHTMLを簡単に描画してくれる仕組みが欲しくなります。これを StatefulComponent
として独自に作成しました。Statefulというのは内部に状態を持っていて、Reactのコンポーネントのように setState()
ができるということですね。
HTMLの描画はどうしてるかと言うと、lit-htmlというES2015のTemplate Literalを利用したHTMLテンプレートの実装を使っています。Reactだとかの仮想DOMの仕組みはMPAで使うにはちょっと大袈裟すぎるので、フットプリントの小さいlit-htmlを採用しました。
FluxのようなComponent間のメッセージングはブラウザのイベントの仕組みをそのまま使っていまして、イベントを受け取った各コンポーネントがそれぞれの内部の状態を適宜更新するということをしています。
最終的にこの StatefulComponent
は全体として400行程度のコンパクトな実装になりました。スライドにソースのリンクを貼っておきましたので興味のある方は読んでもらえればと思います。
次にインフラの構成について簡単にお話します。
クラウドは元々AWSを使っていまして、リニューアル後も特に変える理由もなかったので継続して利用しています。リニューアル後変わった所としましては、以前はEC2のインスタンス上でWordPressを稼動していた所を、ECSを使ってコンテナ上でアプリケーションを稼動させるようにしました。ECSのバックエンドとしましてはEC2を経由せず、直接Dockerコンテナを起動させることのできるFargateを使っています。
あとはデプロイについては、以前はサーバーに直接入ってgit pullを叩くという牧歌的な仕組みだった訳ですが、これをCodePipelineを使ってコンテナのビルドとデプロイを自動化する仕組みを作りました。フローとしてはGitHubのリポジトリのmasterブランチにソースをpushすると、CodeBuildによる各種コンテナのビルドが開始されます。
ビルドに成功するとECRにコンテナイメージが登録されて、そのことをトリガーにデプロイ用のLambda Fcuntionが起動します。CodePipelineにもECSにデプロイする仕組みはあるのですが、これはコンテナイメージのバージョンのみを更新するもので、コンテナに与える各種設定までは変えてくれません。FargateであればCPUとMemoryの指定も含まれます。これを解決するために自前でデプロイする仕組みをLambda Functionで作ったというわけです。
最後にまとめです。
サービスのリニューアルは売り上げが立っているので、サービスの継続可能性は高いだろうと。だから長期間の保守に耐えられる設計が必要になりました。
具体的にはサーバーサイドではフルスタックのフレームワークの利用を避けて、PSRによって標準化されたライブラリを組み合わせて実装。フロントエンドでは、ブラウザ組込みの機能とHTMLテンプレートライブラリであるlit-htmlを使って、フットプリントの小さなコンポーネントを作成。インフラは、コンテナ技術をフル活用しつつ、デプロイも完全自動化して、運用コストを削減しました。
最後に今回はお話する時間はありませんでしたが、サービスのデザインについては長文が読み易いようにということを心掛けてUIを設計しました。
今回の発表は以上となります。ご清聴ありがとうございました。
2024.11.13
週3日働いて年収2,000万稼ぐ元印刷屋のおじさん 好きなことだけして楽に稼ぐ3つのパターン
2024.11.21
40代〜50代の管理職が「部下を承認する」のに苦戦するわけ 職場での「傷つき」をこじらせた世代に必要なこと
2024.11.20
成果が目立つ「攻めのタイプ」ばかり採用しがちな職場 「優秀な人材」を求める人がスルーしているもの
2024.11.20
「元エースの管理職」が若手営業を育てる時に陥りがちな罠 順調なチーム・苦戦するチームの違いから見る、育成のポイント
2024.11.11
自分の「本質的な才能」が見つかる一番簡単な質問 他者から「すごい」と思われても意外と気づかないのが才能
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
2024.11.18
20名の会社でGoogleの採用を真似するのはもったいない 人手不足の時代における「脱能力主義」のヒント
2024.11.19
がんばっているのに伸び悩む営業・成果を出す営業の違い 『無敗営業』著者が教える、つい陥りがちな「思い込み」の罠
2024.11.13
“退職者が出た時の会社の対応”を従業員は見ている 離職防止策の前に見つめ直したい、部下との向き合い方
2024.11.15
好きなことで起業、赤字を膨らませても引くに引けない理由 倒産リスクが一気に高まる、起業でありがちな失敗