Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

「Python FlaskによるWebアプリ開発入門」の紹介

こんにちは、データAI部Pythonエンジニアの工藤( id:irisuinwl )です。 今回は弊社Pythonエンジニアである平田さんが著者の一人であるPython FlaskによるWebアプリ開発入門のご紹介をいたします。

私もレビューに関わり献本頂いたので、その魅力をお伝えできればと思います。

目次

  • 本の概要
    • 内容紹介
    • 誰向け?
    • ここが嬉しい
  • 各部の説明
    • 第0部 イントロダクション
    • 第1部 Flask入門
    • 第2部[Flask実践1]物体検知アプリの開発
    • 第3部[Flask実践2]物体検知機能のAPI化/デプロイメント
    • 第4部 機械学習APIの開発

本の概要

内容紹介

「Python FlaskによるWebアプリ開発入門」はタイトルの通り、Webアプリケーションを作りながらPythonのAPIフレームワークであるFlaskを学んでいく本です。
Flaskの概要と使い方を紹介した後に実践的にWebアプリケーションを作成していきます。

この本で作るアプリケーションとしては以下になります。

  • 認証機能のある簡単な問合せアプリケーション
  • 物体検知アプリケーション
    • アプリケーション実装
    • 機械学習APIの実装
    • テスト作成
  • 機械学習モデルの検討、データ分析コードを移行したWebアプリケーション

様々な機能のアプリケーションを作ることで実践的なFlaskの使い方を学べる非常に良い本だと感じました。

誰向け?

この本を読む上で向いている読者は以下のように思いました

  • 向いてる人

    • Pythonの文法を抑えているが、アプリケーションを作るためのHTTPの知識やインフラの知識が薄く、アプリケーション開発をしてみたい人
    • Pythonでモデル開発をしているが、プロトタイピングのためにAPIとWebアプリケーションを作成したいと思っているデータサイエンティスト
  • 向いてない人

    • Pythonを全く触ったことのない方

ここが嬉しい

この本を読んで嬉しいポイントを紹介します

  • 様々なフレームワークと比較してFlaskを使う利点が紹介されている
  • APIのコードだけでなく、APIを動かすインフラ、テスト、機械学習モデル開発といったWebアプリケーション開発に関連する項目を広範に扱っている
  • 説明が丁寧
    • APIを作る上で必要なHTTPの知識(Cookieやセッションなど)
    • 関連するPEPの提示
    • 関連するコードのディレクトリ構成が明示されていて分かりやすい

非常に説明が丁寧でアプリケーション開発に関わることをなるべく省略せず解説しているため、Pythonを使ってアプリケーション開発を始めようという人にとって非常に学びの深い本であると感じました。

続きを読む

完走したClassi developers Advent Calendar 2021を振り返って今年を締めよう!

挨拶

こんにちは・こんばんは・おはようございます。開発者ブログ編集部長のid:aerealです。

時は師走、ソフトウェアテック界隈ではおなじみとなったアドベントカレンダーの季節でした。

当社もClassi developers Advent Calendar 2021と銘打ち、25本の記事が集まりました。

これまでもClassiでアドベントカレンダーはやっていたのですが、今年は開発者ブログ編集部を立ち上げてから始めてアドベントカレンダーの企画・編集を編集部が執り行いました。 編集部員が5名に拡大したことにより25日間に渡る怒涛のレビューや進捗管理に挑戦できるだろうと踏んでのことでした。

今年は執筆者のユニークユーザー拡大を目標にこれまでアウトプットの経験が少なかったメンバーを中心に声をかけました。 おかげでアドベントカレンダーをきっかけに始めて記事を書いたよというメンバーも増えて何よりです。

今回は無事にアドベントカレンダーを完走したことを祝して編集部で改めて投稿された記事を振り返り、一言ずつコメントで紹介することにしました。 もしまだご覧になっていない記事があったらぜひこの機会に読んでみてください。

振り返り

12/1 id:tetsuro-ito: 開発本部長になってやったこと - tetsuroitoのブログ

id:ClassiJPより

職位の大きな変更に際し、短い期間で何を考えどう動いたかがまとめられています。見返しててあらためて思ったのですが、1ヶ月で100名弱のメンバー全員との1on1をやったり新部署を作ったり、ここに書かれていることだけでもタフなムーブすぎて驚きです。

12/2 沼沢一樹: GitHub に AWS 認証情報を持たせずに、Actions で S3 Backend な Terraform の plan を実行する - Qiita

id:aerealより

ホットな AssumeRoleWithExternalIdentity の話題でした。

こういった認可まわりを変える際って一般的な書き方になっていて文書から仕様を読み取りづらかったり、試行錯誤するのも大変だったりするので身近な人の事例がまとまっているととても助かります。

余談ですがJWTの sub にリポジトリが入っていてtrust policyでそれを参照できるの、JWTの良いところが出ているなあと思います。こういうところが好きです。

12/3 id:tkdn: JSConf JP に参加してきました - Classi開発者ブログ

id:kiryuanzuより

今年の11月に開催された JS Conf についての感想が書かれた記事です。発表を聞いてアクセシビリティについて改めて学んだ話や OSS やソフトウェアが成立するためのプラットフォームの話など、多方面な発表が行われたようで、自分自身は未参加でしたが大変興味をそそられました。年末年始にアーカイブの方で聴講したいと思います!

12/4 Sab: iOS15からのキーボード回避[UIKit]

id:tkdnより

OS のキーボードが表示された際にどうするか、iOS 15 から実装可能になった UIKeyboardLayoutGuide を紹介していただきました。単に機能紹介ではなく、じゃあこれはどうなの?といった気になるところまで掘った良記事でしたね

12/5 id:lime1024: ecspresso をより安全に使うために - らいむぎばたけ

id:tetsuro-itoより

ecspressoを安全に使うためのモチベーションや実際にハマったポイントなどをコードとセットで紹介していて、他の人の参考にもなりそうでした。

12/6 youichiro ogawa: Next.js, Prisma, Apollo GraphQL, Nexusで作るシンプルTODOアプリ

id:ClassiJPより

実際に作りながら、各種ライブラリはどうして、何に使うのかが丁寧に記載してあってとても参考になります。GraphQL Nexusは知らなかったけど便利そう!って思ったのであとで触ってみようと思いました。

12/7 id:kazumeat: リモートワークのための質問力向上研修を実施しました - Classi開発者ブログ

id:kiryuanzuより

今回のアドベントカレンダーで最多ブックマークを記録した記事です🎉

社内で実施された質問力向上研修について大変丁寧かつ分かりやすくまとめていただきました。質問をする際や回答する際に「この伝え方で大丈夫かな?」と困ったらこの記事を見返したいと思います。

12/8 id:kiryuanzu: Hardening 2021 Active Fault 参加レポート - 桐生あんずです

id:aerealより

id:kiryuanzuさんのこの記事をきっかけにHardeningという競技を知ったのですが、実際の業務にかなり近い……というかほぼそのままといって良い内容で精巧さに驚きました。

「セキュリティホール90%オフ」というチーム名はとんちが効いていて好きです。

12/9 id:ruru8net: Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話 - Classi開発者ブログ

id:tkdnより

Datadog 日本の公式 Twitter にも捕捉されていました。バッチの監視といったところだけではなく、社内の Datadog 勉強会でもカスタムスパンを使ったトレーシングの知見も発表していた id:ruru8net のナイス監視実践記事。

2 年前の記事もリンクされており若手の成長を感じる良い記事ですね。

12/10 id:tomoyanamekawa: Cloud Composer 2へのupgradeでどハマりした話 - Classi開発者ブログ

id:testuro-itoより

最近GAになったCloud Composer2のアップグレードに際して、ハマりやすいポイントを解説していて、良かったです。ドキュメントもまだ少ないので、貴重な記事でした。

12/11 id:onigra: EC2からECSへ移行する道のり - Classi開発者ブログ

id:ClassiJPより

成長機会を積極的に取りに行く若手メンバーとそれを導くムーブができる人がいてめちゃめちゃいい会社じゃん〜と思ったら弊社でした。僕も横で見てましたがすごい勢いで作業が進み若者が成長していく様子は、見ててとても気持ちがよかったですしめちゃめちゃ刺激になりますね!

12/12 北原幹也: デザイナーとエンジニアの対話で、UX/UI の意思決定のボトルネックを解消する - Qiita

id:aerealより

「対話がうまくいっていない」という出発点から始まり、物の本を当たりながら「アンチパターンを踏み抜いていた」「ユーザビリティとはなんなのかの定義が揃っていなかった」「デザイナー視点での評価基準をエンジニアが把握できていなかった」と現状理解を深めひとつずつ解決していく丁寧なストーリーでした。

12/13 id:tasmaniadecoco: IAM Policy Simulator で「必要な権限足りてる?」を確かめる - Classi開発者ブログ

id:kiryuanzuより

IAM Policy Simulator についてはよく知らなかったのですが、自分も記事内の同じ課題に直面していたことが少し前にあったので大変参考になりました🙏

レビュー依頼時のコミュニケーション課題に対して問題意識を抱えて解決しようとする姿勢にも真摯な印象を受け、振る舞いや考え方に対しても参考にしていきたいと思える記事でした。

12/14 id:khaigo: UIKitでDesign Systemを実装する - Classi開発者ブログ

id:tkdnより

アプリにおけるデザインシステムを UIKit で実践する記事です。Swift のケースではどうなるかも記載があって静的型付けしたいよな〜わかる〜という感想が湧いてきました笑 今後の伸長に期待ですね!

tetoru(テトル) に関わる内容が開発者ブログでもちらほらと… id:sasata299エンジニアに役割を変えた記事でも触れています。

12/15 おかじ: 入社前に不安だったリモートワークでのコミュニケーションは実際どうだったのか

id:tetsuro-itoより

リモートワークにおいて、コミュニケーションが重視される中で、とても示唆に富む記事でした。リモート時に入社したことがある人とそうじゃない人のナラティブに端を架けてくれる記事で、いろんな人に読んでもらいたいですね。

12/16 id:seiga_hayashi: 育休を取得することにしたらチームの状態が改善した話 - 目だ!目を狙え!

id:ClassiJPより

とても大事な育児休業についてと、それを取得するために社内でどのような準備をしたかについて解説しています。これから育児休業を取る方の参考になる点もあるのではないでしょうか。

12/17 id:c5meg1012: バックエンドエンジニアが基盤インフラチームに異動して半年ほど経った話 - めるノート

id:aerealより

id:c5meg1012さんとは隣のチームで仕事しているのですが、産休・育休から復帰のタイミングでキャリアチェンジも図るというかなり意欲的な態度で驚くと同時に着実にキャッチアップされていて負けていられないなという気持ちになります。 ちなみにその後、AWS認定 ソリューションアーキテクト アソシエイトに合格されたそうです。

12/18 id:ttakagi1021: トピックモデルを使って問い合わせ内容を分析した話 - Classi開発者ブログ

id:kiryuanzuより

Classi を利用されているお客様からの問い合わせ内容を言語処理技術の一つであるトピックモデルを使って分析した記事です。分析の流れをわかりやすく述べており、その分析結果から今の課題や施策の効果などの考察について説明されています。 データ分析の活動から効果的な施策を検討していく流れの一例を知ることができ、大変面白い内容でした。

12/19 横田貴之: SwiftLintをGithubActionsのmacOSではなくubuntuで実行する

id:tkdnより

GitHub Actions の OS イメージが macOS になることのコスト感どこも課題に感じることはありますね。SwiftLint を動作させるためのステップが詳しく紹介されています。記事内に登場した GitHub Actions におけるトークンのパーミッションの制御は今年できるようになったんですよね。

id:aerealハマっていたようなので GitHub Actions の実行権限について考える場合はご注意を!

12/20 陳巍: 伝わるAPIドキュメントを作成する - Qiita

id:tetsuro-itoより

OpenAPIで伝わるための工夫を書いてくれました。育児でなかなか時間を作れない中でとてもよかったし、こういった工夫の繰り返しが円滑なAPI設計やコミュニケーションの流通を促しますね。

12/21 id:irisuinwl: FirestoreのCRUD APIを作って、負荷試験をする - irisuinwl’s diary

id:ClassiJPより

自作の負荷テスト用アプリケーションの解説にとどまらず、Firestoreとはどんなものかや負荷テストする上での考慮点等にも触れられていてとてもよかったです!

関係ないですが、本記事を読んでこういうツールがあるんだ〜と思ってlocustで検索したら虫の画像が出てきたのでちょっと慌てました。

12/22 id:nakaearth: 推理小説をグルーピングするために特徴語を抽出してみる - nakaearthの日記

id:aerealより

特徴語抽出して遊ぶシリーズは定番ともいえますが、ミステリ小説をかけてみるという発想がおもしろいですね。 word2vecなんかを使って意味・構文埋め込みをして叙述トリックが使われている作品とそれ以外の作品でなんらかの特徴が浮かんでくるんだろうか? という着想を得ました。

12/23 id:minhquang4334: ISUCON11予選課題の27万点まで練習し新人エンジニアが学んだこと - Classi開発者ブログ

id:kiryuanzuより

2020年秋に新卒として入社された id:minhquang4334 さんによる ISUCON11 の振り返り・復習記事です。ISUCON初参加で100/598位という結果も残されていることも大変凄いのですが、その後さらに知らなかったことに対して満遍なく吸収する姿勢が本当に素晴らしく、自分も見習おうとなりました。

12/24 id:JesseTetsuya: Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話 - Classi開発者ブログ

id:tkdnより

Excel 運用されていたデータのマスター管理によって調査やデバッグに時間がかかっていた課題、Excel に RDB のテーブルスキーマが整理されていたのがつらいという課題へソフトウェアを介入させ、まさに「ザ・カッとなってやった課題解決!」といった記事で気持ちがいいですね。

12/25 id:nkgt_chkonk: 2021年Classiに起こった変化の振り返り - Classi開発者ブログ

id:tetsuro-itoより

今年の技術視点でのふりかえり記事でした。読みながら、今年も変化してきたなと内省することができたし、これからの変化の仕込みにも期待ですね!

おわりに

記事投稿時にレビュアーとして目を通す機会もあり、その時に気付いたことがひとつあります。 それはこれまでに投稿した記事へ言及し「今はこうだよ」「これに刺激を受けてやってみたよ」といった内容が多かったことです。

実際、社内でも「○○さんが詳しいよ、前にこういう記事を書いていて──」と紹介するようなシーンがここ1年で増えたという実感があります。

それぞれ別々のチームで働いていてもやっていること・関心のあることに連続性があると改めて実感します。 そしてこの感覚を編集部だけではなく記事を書いたメンバーにもそれぞれ感じてもらえたことで、アウトプットを積み重ねることの大きな意義が少しでも伝わったのであれば編集部冥利に尽きます。

アドベントカレンダーというお祭りはまた1年後までのお預けになりますが、Classiはこれからも着実にアウトプットを通して研鑽していきます。 それではみなさま良いお年を!

2021年Classiに起こった変化の振り返り

VPoTの id:nkgt_chkonk です。この記事は Classi developers Advent Calendar 2021 25日目の記事です。昨日は平田(@JesseTetsuya)さんによるFlask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話でした。

さて、あと少しで2021年も終わってしまいますが、2021年、みなさんはいかがお過ごしでしょうか。Classiにとっては去年から引き続き「変化の年」だったなあ、というのを強く感じています。本記事では、この一年、Classiがどのような課題に直面し、どう変化してきたのかを振り返ってみたいと思います。

年明けから春まで

去年の話になってしまいますが、Classiは2020年4月に、大型アクセス障害を起こしてしまいました。この反省から、同じような事故でユーザーのみなさまにご迷惑をおかけしてしまうような事態を繰り返さないために、対症療法ではなく根治療法として組織の形の変革や仕組みの変更を行いました。その詳細は2020年11月に書いた セキュリティインシデントと大規模障害を経てClassiは開発組織をどう変化させたのか - Classi開発者ブログ に譲ります。

その後、年明けから今年の春に向けて、新しくなった組織で、各チーム、各メンバーはとにかくシステムの課題の改善に必死にとりくんでくれました。わたしにとって印象的だった例としては、「スーパーN+1」「大迂回が必要な改修」や「オブザーバビリティへの継続的な取り組み」があります。

スーパーN+1

まず、「スーパーN+1」です。Classiには、社内で「スーパーN+1」と呼んでいた高いシステム負荷を引き起こす問題が散見されていました。あるユーザーからの1リクエストに対して、パラメータを変えながら何回も内部APIを呼び出します。この時点でAPIに対するN+1が起こっているのですが、なんとその内部APIでも、DBに対してN+1問題を引き起こしているような問題です。

スーパーN+1を改修するためには、APIを呼んでいる側呼ばれている側双方にまたがる改修を行う必要があるため、修正が高コストになりがちです。また、Classiでは、自動テストが不足しており、このこともスーパーN+1を改修するにあたって修正の高コスト化を呼んでいました。それだけではなく、以前のClassiはデプロイが事故りやすかったりロールバックに時間がかかったりというデプロイ上の問題も抱えていました。これも、スーパーN+1を解消するための修正コストを跳ね上げていたひとつの原因です。

そんな中で、デプロイ上の問題を解決するためにECS化を進めたり、それと同時進行でスーパーN+1を解消したりと、自分たちの管理しているサービスに対して適切にシステム上の問題を解決していったメンバーを見ていて、大変心強かったのをよく覚えています。このように、日々きちんとシステム上の問題が解決されていくような光景は、2020年春以前にはなかなか見ることができない光景でした。本当に心強く「Classiって変わったな」と思うことが多い日々でした。

大迂回が必要な改修

次に「大迂回が必要な改修」です。実はClassiには、RDB上でデッドロックを引き起こしてしまう機能が存在していました。その機能はデッドロックを引き起こしてしまうということの他にも、ユーザーにとって不利益となる根本的な仕様上の問題を抱えており「この機能はなくしてしまうべきだ」という判断がなされました。

しかし、その機能を無くしてしまうことにより、不利益を被ってしまうユーザーも当然存在するわけで「いまある機能とは別のやり方で、その機能で実現できていたことを実現させる」という必要が出てきます。つまり「問題を起こしている機能をなくすためには、別の機能をリリースしてからその機能を無くさなければならない」という、迂回が必要になるわけです。

このようなロングスパンの改善も、しっかりとやり切ってくれて「コストがかかるけどわかりやすく新しい価値が生まれるわけではない。しかし問題は抱えている」というような部分の改修をきちんとやりきれたことも、Classiの大きな変化を感じる例でした。

オブザーバビリティへの継続的な取り組み

最後に「オブザーバビリティへの継続的な取り組み」があります。これは id:ruru8net によるAmazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話などがわかりやすいかと思います。

現在のClassiでは、なんらかの障害が起こったあと、レトロスペクティブで「これを検知できていたら傷は浅かった」みたいな項目が見つかるたびに、そのための監視の仕組みが整備されるような動きが継続して行われています。これも「そこに投資しても直接わかりやすく新しい価値を産むわけではないが、将来の障害を減らし、素早くプロダクトを進化させていくためには必要」という種類の投資です。このような投資が当たり前にされるようになったことに、やはり大規模アクセス障害以前との変化を強く感じています。

このような変化をメンバーひとりひとりが自発的に起こしてくれた結果として、今年の春は去年よりも大きなトラフィックがあったにも関わらず、大きな事故なく春のピークを乗り切ることができました。これは本当にメンバーひとりひとりが尽力してくれたおかげだと思っており、とても感謝しています。

とくに、システム改修のために力をたくさん発揮してくれた若手のエンジニアが、今やClassiに欠かすことのできない戦力として活躍してくれていることが本当に嬉しく、心強く思っています。

夏〜秋

さて、春にアクセスのピークを乗り換えたあと、苦しい時期が続いたと思っています。というのも、システム面の問題の改善は、マイナスをゼロに近づけるための仕事です。一方、ユーザーの抱えている課題を解決する為には、システムの機能適合性や使用性の向上のような「ゼロをプラスに」する仕事もしなければならないわけです。

「去年のような事故を起こさない」という大きな目標をひとつ越えましたが、まだまだ直さないといつか爆発してしまうシステム課題も残っています。この課題に取り組みながら、システムの機能適合性や使用性を向上させていく取り組みが、夏〜秋にかけて多くなってきました。

ここで課題になってきたのが、リポジトリ単位でチームが組成されていることと、分断され絡み合ったモノリスになっていることです。

Classiのリポジトリは、基本的には機能単位で分かれているのですが、中央に巨大なDBが存在し、そのDBを各リポジトリで共有しています。一方、「機能単位」とはいえ、Classiというひとつの大きなプロダクトに対して「ひとつの機能」だけで価値を提供することは難しく、さまざまな形でリポジトリ同士が絡まり合っています。

また、「Classi」という大きなひとつのプロダクトで使用性や機能適合性を向上させるためには、探索的にユーザーの課題を解いていく必要があります。そのため、これらの向上のためには、システム課題の改修以上にリポジトリを超えた柔軟な協働が必要となります。

さらに、分断され絡み合ったモノリスの中では、影響範囲が分かりにくい中で、自分のリポジトリだけではなくて全体を見ながらやりたいことを達成するための、技術的な戦術を描けることが必要になります。このような戦略を描くためには、アーキテクトと呼ばれるようなひとたちが持っている、かなり高度かつ広範囲なエンジニアリング能力が必要です。そして、各チームが自律的にその動きをできるようにならないと、Classiの進化をスケールさせることはできません。

また、各チームには各チームのやりたいことがあって、しかし分断され絡み合ったモノリスの中ではそのやりたいことを実現しようとするとどうしても他チームの協力が必要となります。しかしそのチームにもやりたいことはありますので、チーム同士の利害がバッティングしてしまうことが多発します。

こういった技術的な課題、組織的な課題が健在化してきたのが、夏〜秋にかけての苦しい時間だったと感じています。また、この時自分は、まだVPoTとして解くべき課題がわかっていなかった、と反省しきりです。わたしがVPoTとしてすべきことを模索している中で、チームのメンバーは組織に起因する課題にぶつかって日々苦しんでいました。

「みんな全力で頑張ってくれているのに、どうしてもうまく成果につながらない」という時期を過ごすことで「みんなが全力で頑張ってくれたらその分だけちゃんと成果がでる仕組みを作ること」が自分の仕事のひとつなのだ、と理解することができたのですが、それを自分が学んでいる間は、本当にメンバーに苦しい思いをさせてしまいました。

秋〜今にかけて

「みんなが全力で頑張ってくれたらその分だけちゃんと成果がでる仕組みを作ること」が自分のVPoTとしての仕事なのだ、と理解したのち、まずは「なぜ頑張ってくれているのに成果に結びつけることができていないのか、それを解決するためにはどういう構造をとればいいと思っているのか」の自分なりの分析を社内に対して発信し、社内のさまざまな立場の人と対話をし始めました。この発信と対話の中から生まれたひとつの解が「チーム横断バックログ」として現在運用され始めています。

このチーム横断のバックログは、チームの利害関係がバッティングしスタックしてしまう課題に対して「まずはどのチームがどんな解きたい問題を持っていて、そのためにどのチームの協力が必要なのかを可視化、管理する必要がある」と考え、作成しました。このバックログを作るにあたって、作ると決めて考えるまでは簡単だけど、全社の理解を得て実効させていくのはかなりカロリーの高い仕事であるということも学びました。個人的な話になってしまいますが、こういう高カロリーな仕事は片手間ではなかなか進めることはできないので、自分のような責任者がきちんと進めていくことが必要で、それが今現在のClassiでVPoTがやるべき仕事のひとつなのであろうと学んだ時期でもあります。

ただ、このチーム横断のバックログはまだ運用がへろへろで、その改善は継続的に必要だとも考えています。いつか続報をお知らせできたら嬉しいです。

さて、チーム横断のバックログがあることで、チームごとの利害がバッティングしたときの調整は以前よりもうまくできるようになりました。しかし、それだけでは、組織的な問題はある程度解決しても、チームがそれぞれアーキテクト的な動きをできなければ結局物事が進まない問題は未解決となってしまいます。

ここに対しては、ふたつのアプローチを現在考えています。ひとつは、技術的なアプローチ、もうひとつは組織的なアプローチです。

まずは技術的なアプローチに関してです。そもそもなぜこんなにも機能開発やシステム修正の難易度が高いのか。それは、内部品質の低さに依るものです。内部品質が高ければ高いほど、プロダクトイシューを解決するために必要となる技術的難易度は下がっていきます。なので、技術面でいえば、「外部品質や利用時の品質につなげるために、内部品質を向上させること」が必要なわけです。

弊社のプロダクトは、お世辞にも変更容易性やテスト容易性が高いわけではありません。ここに対して、スケジュールを切った上でまずは自動テストの拡充から始めています。今のままだと、高度かつ広範囲なエンジニアリング能力がプロダクトイシューの解決に必要となってしまうわけですが、内部品質を継続的に高めることによって、より多くのメンバーのより多くの力がプロダクトイシューの解決に寄与できるようになることを目論んでいます。

一方、組織的なアプローチとしては「では目下足りていない内部品質に対して、2022年度はどのようなチーム構造を作り、どのような仕事の進め方をすれば、最も素早くプロダクトイシューを解決できるのか」の戦略を各組織長とともに立て、実効させていくための立て付けを、目下構築中です。

まとめ

今年一年を振り返ってみましたが、ひとりひとりのメンバーが課題感を持って動いてくれた結果、Classiは本当に変わったと実感します。結果として、新しい問題が立ちはだかっている状態ではあるのですが、これはつまり「ボトルネックが移動した」というやつだと思っているので、前向きに、新しくでてきたボトルネックに対応し続けていけるVPoTとなっていきたいと思っています。その結果、来年はさらなる変化を起こして、もっとユーザーの感じる価値に真摯に向き合い素早くプロダクトを進化させていけるようになりたいし、そのための仕組みをきちんと作っていくのがぼくの仕事なので、周りに助けを求めながらしっかりその職責を果たしたいと考えています。

最後に、今回の記事で赤裸々に書いた通り、現在のClassiは決して技術的にも組織的にも「理想的な環境」とは言い難いです。しかし、一から綺麗な環境を作るよりも、すでに問題を抱えている環境をより良い環境にしていくことは技術的にたいへんチャレンジングな課題だと私は考えていて「課題解決ジャンキー」にとっては大変に刺激的な組織であると思っています。課題解決がやりたいひとにとっては本当に無限に機会があるので、自分の力で大きな課題を解決したいみなさまはぜひ! 採用情報 | Classi(クラッシー) - 新しい学びが広がる未来の教育プラットフォームを創る からご連絡いただければ幸いです! カジュアルにお話しするところから始めましょう!

Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作ってみたら役立った話

この記事はClassi developers Advent Calendar 2021の24日目の記事です。

こんにちは、データAI部でPythonエンジニアをしている平田(@JesseTetsuya)です。普段は、PoCとデータをもってくる、というところ以外全部やる、というスタンスで開発業務を行っています。

今回は、Flask App Builderでコンテンツマネジメントシステムとメタデータマネジメントシステムをさくっと作っておいたら、諸々あとから役立った話をしていきます。

コンテンツマネジメントシステムとは、WEBテストで利用する一部のコンテンツを管理をする社内用システムになります。メタデータマネジメントシステムとは、データ分析基盤のテーブル情報やカラム情報を管理するシステムになります。

まずは、それらシステムの土台となっているFlask App Builderの説明をしていきます。

Flask App Builderの概要

Flask App Builderは、Flask製のCRUDアプリケーションをさくっと作れるオープンソースです。

Flask App Builderのドキュメントはこちらです。インターフェースは、こんな感じです。

f:id:JesseTetsuya:20211223153926p:plain

この画像は、リポジトリ内にあるサンプルの一つです。コードはここにあります。

上記のサンプル以外にも、それぞれのユースケースごとの機能が実装されたサンプルコードはここにあります。

Flask App Builderって何ができるのかを理解するには、下記のREADMEを読めば概要を理解できるかと思います。

f:id:JesseTetsuya:20211223153955p:plain

下記コマンドでさくっと試してみることができます。 わかりにくい方は、こちらのyoutubeの解説動画をおすすめします。

(venv)$ pip install flask-appbuilder
(venv)$ flask fab create-app
Your new app name: first_app
Your engine type, SQLAlchemy or MongoEngine [SQLAlchemy]:
Downloaded the skeleton app, good coding!

(venv)$ cd first_app
(venv)$ export FLASK_APP=app
(venv)$ flask fab create-admin
Username [admin]:
User first name [admin]:
User last name [user]:
Email [admin@fab.org]:
Password:
Repeat for confirmation:

(venv)$ flask run

データのCRUD検索処理ユーザーの認証権限管理のようなフルスクラッチで実装すると手間がかかる機能はデフォルトで用意されており、UIも見やすい状態になっています。

このFlask App Builderをカスタマイズして、コンテンツマネジメントシステムとメタデータマネジメントシステムの開発からインフラ設計・構築までを一人でさっと作ってみてどのように役に立っているのかを書いていきます。

まずは、コンテンツマネジメントシステムです。

コンテンツマネジメントシステムの概要

作ったものは、こちらです。 コンテンツ情報が一覧できる画面の写真です。他にももっと閲覧できる画面は、あります。

f:id:JesseTetsuya:20211223154944p:plain

最初は、Google Kubernetes Engine(GKE)の1クラスターまるごと使っておりましたが、インフラコスト的に贅沢すぎましたので現在は、他のアプリケーションもデプロイされているGKEクラスターの社内用ツールのnamespaceを切ってそのnamespace内のpodにデプロイしています。

既に機能が揃っているのでさっとモックを作って見せながら要件をヒアリングしつつ、開発していきました。

他チームからデータや機能の要望があれば、改善を都度行っています。

コンテンツのマスターデータ以外にもWEBテスト機能に必要なデータへのCRUDができるようになっています。

では、コンテンツマネジメントシステムを作って何が嬉しかったか、という話をします。

コンテンツマネジメントシステムをつかった課題解決

コンテンツマネジメントシステムをつかった課題解決は、以下です。

課題

  • 生徒が解く問題のマスター情報を別のチームがエクセルで管理しており、最新の状態の中身を確認するのに手間だった。

解決

  • 一通りのWEB上で問題マスターデータに対してCRUD、検索、フィルター、並び替え、ファイルアップロードなどの機能を追加することで、Excelファイルの散在やコミュニケーションコストや日頃の調査やデバック工数を減らすことができた。

開発工数

  • オープンソースなのでアプリケーション自体は、無料.

  • アプリケーション実装に2, 3人日、インフラ構築に2週間ぐらい。

その他役立ったこと

  • QA実施の際にQAチームにデータ確認をしてもらうのに役立った。
  • WEBテスト機能の開発者がデバックするや仕様確認する際に役立った。

本来の調査やデバックならば、エンジニアが踏み台サーバーにはいってそこからmysqlコマンド叩いて確認してなどの作業が必要ですが、このコンテンツマネジメントシステムのおかげで、さくっとテストデータを入れての確認などがしやすくなりました。

現在では、運用保守の状態で必要に応じて機能を追加していくというような状態です。 今後は、エンジニアバックグラウンドのない方でも利用しやすいようなUIにより改善していければと思います。

次に、メタデータマネジメントシステムのほうをみていきます。

メタデータマネジメントシステムの概要

作ったものは、こちらです。 テーブル名やカラム名は、BigQueryのAPIを叩いてデータ分析基盤から自動連携しています。Cloud Runにデプロイして利用しています。

各テーブルの役割情報やカラム情報などは、自分達で記入していきます。

f:id:JesseTetsuya:20211223155013p:plain

f:id:JesseTetsuya:20211223155024p:plain

ログイン数などの統計情報もいい感じにカスタマイズしやすいコードになっています。 しかし、Redashなどのダッシュボードで見てしまうため、あまりつかっていません。

メタデータマネジメントシステムをつかった課題解決

コンテンツマネジメントシステムをつかった課題解決は、以下です。

課題

  • データ分析基盤を開発する前にそもそもの現状のRDBのテーブル・カラム情報が整理されていなかったので、一人で調べたり、各機能の担当者に話しを聞いて情報をかき集めてエクセルにまとめていました。ただただ、それがつらかった。

f:id:JesseTetsuya:20211223155059p:plain

解決

  • データ分析基盤との自動連携を実装することで自分でテーブル名やカラム名やデータ型から記入する必要はなくなりました。検索性やユーザビリティは、あがりました。

開発工数

  • オープンソースなのでアプリケーション自体は、無料.

  • アプリケーション実装に2, 3人日、インフラ構築に2週間ぐらい。

その他役立ったこと

  • 担当以外のデータをデータ分析基盤でみるときや、ダッシュボード作成の際のクエリを書く時にどんなテーブル情報やカラム情報があって、どんな意味があるのかを理解するのに役立つようになった。

  • そもそも、どんなデータがあるのかわからないという人にとって理解の手助けになった。

テーブル情報やカラム情報は、都度入力していく必要があります。

その施策として、もくもく会を開催したり、チームの定例で10分だけもくもくと手を動かす時間をもうけていきました。

進め方は、meetのブレイクアウトセッションを利用して3,4人のグループに分けて、お互に情報をかけ集めながらやっていきます。

9人でもくもく記入していくと10分で大体一つのデータマートの50%ぐらいのテーブル情報とカラム情報は、記入しきることができます。

このチームでは、3つのデータウェアハウス含めたデータセットのテーブル情報とカラム情報を埋めていきました。一つのデータセットあたり20 - 30ぐらいのテーブルがあります。

これら全て埋めるのに、大体10分のもくもく会を5,6回ほどやると必要なテーブル情報とカラム情報をうめることができました

エクセルに私一人で記入している時は、カラム情報まで記入できず、なおかつ2つのデータセットのテーブル情報まで記入するのが限界でした。

まとめ

アプリケーション実装よりもインフラ構築の方が工数がかかっています。

これは、当時の私のGKEとCloud Runの仕様の知識不足も影響しているかと思いますが、全体としても早く安く作れたCRUDアプリケーションなのではないかと思います。

また、世の中には、既製のCMSやメタデータマネジメントシステムのOSSは多くあるかと思いますが、機能が多すぎるとか、クリティカルな制約があったりするものです。必要最低限の機能をさくっと安く作るには、こういったフレームワークは最適です。

FlaskのみでWEBアプリケーションを開発してみたいという方は、2022年1月24日発売の「Python FlaskによるWebアプリ開発入門 物体検知アプリ&機械学習APIの作り方 」という私が執筆した書籍の一読をおすすめします。

www.amazon.co.jp

また、Classiでは、Pythonエンジニア募集中です。ご興味ある方は、下記のリンクからの応募をお待ちしております。

hrmos.co

ISUCON11予選課題の27万点まで練習し新人エンジニアが学んだこと

この記事は Classi developers Advent Calendar 2021 の23日目の記事です。

こんにちは、プロダクト開発本部の2年目の@minhquang4334です。 今年の8月に、同じ部で3年目の@henchiyb 先輩と一緒に yasuoチームを作り、ISUCON11 オンライン予選に初めて参加しました。参加するきっかけは弊社に業務委託として来てくださっている@soudaiさんからISUCONの話について聞かれて、面白そうなので、チャレンジしてみました。結果はRubyで4万点まで達成できましたが、全体のチームの100/598 位ぐらいで敗退してしまいました。

オンライン予選が終わった後、数百万点を達成したチームはどうやってそこまで出来たのかとずっと疑問でした。各チームの解説ブログを見てみましたが、目を通しただけですぐ忘れてしまい、知見を深く理解できないと思いました。それで、自分でももっと深くやってみたいという気持ちになったので、ISUCON 運営の方が準備してくれた環境を立ち上げて練習しました。再度の結果はRubyで、27万点 (11/598 位の点数相当) まで伸ばせました。

ISUCON11予選の課題の練習を通じて、Webパフォーマンスチューニングを中心に、いろいろ勉強になりました。その知見は新人エンジニアの時とISUCONに初めて参加する時に早めに知っておくと良さそうだと思ったので、この記事ではそんなエンジニアを対象にISUCON11予選課題の解説を含めて、自分なりの学んだことをまとめて共有します。

前提

推測ではなく測定しよう

パフォーマンスチューニングするとき、どのように進めていますか。今までは、私はよくコードを見て、N+1や不要なループなどといったパフォーマンスの悪いコードを気づいたら、すぐ改善しようと思いました。しかし、ISUCONでそのように時々たくさん改善できても、システムのパフォーマンスが上がらなく、点数もほぼ変わっていません。 理由は解決した問題はシステムのパフォーマンス低下の原因ではないからです。それも、問題を特定するために、システムのメトリクスを見ておらず、自分の推測を信じたからです。 例えば、以下のAPIの実行時間フレームグラフの場合、どれほど 不要なループとN+1を改善しても、そのAPI自体における改善の効果は少ないのではないでしょうか。不要なループとN+1は良くない実装ですが、パフォーマンス低下の原因ではありません。

f:id:minhquang4334:20211222190216p:plain
例:リクエストの実行時間

正しい問題を調べるのがすごく大事で、問題解決のアプローチを変更しなくてはいけません。推測の代わりに、システム運用のメトリクスを監視したり、測定したり、異常なものを問題として抽出したりすることです。そうして適切な問題を取り除き、解決してから、再度その測定・改善プロセスを回します。

ISUCON11予選練習の時に、その測定・改善プロセスを回してみると、徐々に点数が上がってきて、やりながら達成感も感じられました。

測定した例です。

  • ALPを使って、リクエストごとに実行時間を測定する
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |  SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
| 32656 | POST   | /api/condition/*             |   0 | 7510 |   0 | 25146 |   0 | 0.004 | 0.320 | 3067.503 | 0.094 | 0.060 | 0.100 | 0.100 |  0.018 |     0.000 |     14.000 |       28.000 |     0.001 |
|  2688 | GET    | /isu/*                       |   0 | 2561 |   0 |   127 |   0 | 0.048 | 0.956 |  549.019 | 0.204 | 0.000 | 0.472 | 0.112 |  0.198 |     0.000 | 135259.000 | 42280309.000 | 15729.282 |
|   639 | GET    | /api/trend                   |   0 |  614 |   0 |    25 |   0 | 0.024 | 1.116 |  397.817 | 0.623 | 0.068 | 0.540 | 0.700 |  0.195 |     0.000 |   4637.000 |  2749921.000 |  4303.476 |
+-------+--------+------------------------------+-----+------+-----+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+--------------+-----------+
Count: 68432  Time=0.00s (28s)  Lock=0.00s (0s)  Rows_sent=46.7 (3194156), Rows_examined=397.0 (27169268), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC

Count: 1089  Time=0.00s (2s)  Lock=0.00s (0s)  Rows_sent=0.0 (0), Rows_examined=0.0 (0), Rows_affected=0.0 (0), isucon[isucon]@localhost
  COMMIT

Count: 1152  Time=0.00s (1s)  Lock=0.00s (0s)  Rows_sent=1.0 (1125), Rows_examined=2725.9 (3140278), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY `timestamp` DESC LIMIT N

Count: 21800  Time=0.00s (1s)  Lock=0.00s (0s)  Rows_sent=3.1 (68432), Rows_examined=0.4 (8204), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu` WHERE `character` = 'S'

Count: 3377  Time=0.00s (0s)  Lock=0.00s (0s)  Rows_sent=0.0 (0), Rows_examined=0.0 (0), Rows_affected=1.0 (3377), isucon[isucon]@localhost
  INSERT INTO `isu_condition` (`jia_isu_uuid`, `timestamp`, `is_sitting`, `condition`, `message`) VALUES ('S', 'S', TRUE, 'S', 'S')
  • top コマンドでCPU 利用率とメモリ利用率を測定する
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2244 isucon    20   0  784600  74604  12984 S 110.6   2.0   0:24.22 bundle
   2077 mysql     20   0 1714832 109468  18660 S  25.9   2.9   0:29.95 mysqld
   2236 www-data  20   0   13792  10416   5036 S   6.6   0.3   0:01.92 nginx
   2235 www-data  20   0   11428   7992   5036 R   3.3   0.2   0:00.90 nginx

もしDatadogやNew Relicなどが活用できたら、測定がより楽になると思います。

データベースについて学んだこと

Webシステムの世界ではデータベース がパフォーマンス低下の原因となるケースは少なくないようです。それで、ソフトウェアエンジニアとして、データベースの正しい扱い方を学ぶのは損ではないと思います。

LIMIT句が大事だ

You aren't gonna need it https://ja.wikipedia.org/wiki/YAGNI

実際に必要となるまでは追加しないのがよいというプログラミング原則があります。そのような考え方でデータベースからデータを取得する時にも、必要であるデータのみを取得するのが大事です。 LIMIT 句を付けたら、 SELECT 文を実行した時に取得するデータの行数の上限を設定することができます。LIMIT 句を付けないと、無駄なデータを取得し、SQLが実行される時間のほとんどがDisk I/Oになってしまって、CPU利用率なども上がって、システムのボトルネックになるケースが多いです。 ISUCON 11で不要なデータを取得しないようにLIMIT 句を付けたら結構パフォーマンスが上がって、点数も急増できました。正に小さい変更で大きな効果です。

  • LIMIT句を付ける前 MySQL Slow Log
Count: 75480  Time=0.00s (2s)  Lock=0.00s (0s)  Rows_sent=46.9 (3537598), Rows_examined=3.0 (228652), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC
  • LIMIT句を付ける後 MySQL Slow Log
Count: 226516  Time=0.00s (8s)  Lock=0.00s (1s)  Rows_sent=1.0 (223303), Rows_examined=0.1 (29024), Rows_affected=0.0 (0), isucon[isucon]@localhost
  SELECT * FROM `isu_condition` WHERE `jia_isu_uuid` = 'S' ORDER BY timestamp DESC LIMIT N

LIMIT句を付ける後、Rows_sent と Rows_examined の数値が大きく減らすことができました。見なきゃいけないデータが限定されて、データベースの負荷も大きく減らせます。以下は実装・測定の参考となります。

Auto Increment Primary Key を使うべきか

Primary Key カラムにAUTO_INCREMENT をつけると、データを追加した時にカラムに対して現在格納されている最大の数値に 1 を追加した数値を自動で格納することができます。カラムに連続した数値を自動で格納したい場合に便利ですが、パフォーマンス的に問題ないでしょうか。MySQLの前提で考えてみましょう。

MySQLでB-treeインデックスの構造とInnoDBストレージエンジンを使われています。B-treeは以下の写真のような木のデータ構造で、テーブルのIndex (Primary Keyも一つのIndex) をテーブルのデータと別途に管理するものです。テーブルに一つのレコードを追加したら、テーブルにあるIndexのB-treeにも一つのノードが追加されます。B-treeの詳しい解説はこちらをご覧ください。

f:id:minhquang4334:20211222190032p:plain
B-tree インデックス構造

B-treeにノードが追加されるときに、どこで保存できるか既存のノードからそれぞれ比較します。それで、Primary KeyにAUTO_INCREMENTをつけたら、Indexは連続した数値なので、すぐ保存できるポジション (一番右側の下)を見つけられます。その特徴からAUTO INCREMENT Primary Keyを使ったら、Insert文が早く大量のInsert文によって負荷が高くなるシステム (write-heavyシステム) に相応しいと思っている方は多いのではないでしょうか。 しかし、InnoDBではACID特性)を守るため、AUTO_INCREMENT ロックモードがあります。それはテーブルにレコードを書き込むときに、AUTO_INCREMENTのロックをとって、他の書き込むトランザクションが待機させるという仕組みです。

f:id:minhquang4334:20211222190307p:plain
Mysql AUTO_INCREMENT ロックの例

上記の写真のように、同時に複数のトランザクションからAUTO INCREMENT Primary KeyがあるテーブルにInsert文が発行されたら、T1が終わるまで、T2がロックされます。T1が終わったら、T2でロックが 解放されて、T2はAUTO_INCREMENTロックを取得できて、Insert文が発行できます。そのため、大量のInsert文の負荷があるシステムなら、AUTO INCREMENT Primary Keyを使う場合、副作用が出てくる可能性があり、パフォーマンス低下の原因の一つになります。

ISUCON11でIOTシステムみたいな時系列のデータを収集するシステムなので、AUTO INCREMENT Primary Keyのテーブルに同時に大量のInsert文が発行されました。そのテーブルのPrimary KeyをCompound Keyに変更したら、結構パフォーマンスが改善できました。

システムアーキテクチャについて学んだこと

一個一個のAPIやリクエストやクエリなどを改善するというより、システム全体の構成に問題があるのか特定して解決できたら、ISUCONだけではなく実際にシステムのパフォーマンスを最適化するうえで大きなカギになると思います。

コンテンツ配信はNGINXに肩代わりさせる

普通のWeb システムのクライアントに静的なファイルを配信する方法は root/ フォルダーにファイルを置いておき、必要な場合はバックエンドがユーザーの認証やファイルを読み取りをしてから、Webサーバーが配信します。 Rubyの場合は、send_fileというメソッドがよく使われています。

railsdoc.com

指定したパスに存在する画像やファイルを読み込み、その内容をクライアントに送信

send_fileの動きが気になったことはありませんか。これは、ディスクからファイルを読み取る必要があることです。このファイルは、出力バッファーを通過し、Webサーバーにフラッシュされ、クライアントに配信します。大きいファイルの場合、メモリをたくさん使って、memory_limitを超えたケースもあります。ファイル全体をメモリに保存したり、削除したりするプロセスのため、バックエンドが忙しくなって、他のリクエストを処理できなくなります。

そこで、バックエンドの負荷を減らすために、X-Accel-Redirectを使って、認証のみをバックエンドで行い、コンテンツ配信はNginxに肩代わりさせることができます。X-Accel-RedirectはNginxの機能で、バックエンドから返されたヘッダによって決定される場所への内部的なリダイレクトを許可します。それでバックエンドを解放して他のリクエストを処理でき、Nginxが持つ素晴らしいコンテンツ配信の力で、高速配信を実現できます。

f:id:minhquang4334:20211222190559p:plain
X-Accel-Redirectを使った場合

ISUCON11予選を練習するときに、その機能は全く知らず、解説しているブログなどをいくつも読むことで勉強になりました。以下のようにnginx設定ファイルに設定しました。

https://github.com/minhquang4334/isucon11-training/blob/master/isucondition.conf#L64-L68

    location /icon {
        internal;
        alias /home/isucon/tmp; 
        expires 86400s;
    }

location /iconinternalを追加したら、X-Accel-Redirect レスポンスヘッダーを返すことでnginxから静的ファイルをクライアントに配信できます。

リバースプロキシキャッシュを用意する

キャッシングはWebパフォーマンスの最適化最適化におけるポピュラーな手法の一つです。現代のWebシステムにキャッシングシステムはなくてはならない存在になったといった印象もあります。それを用意することで、Webサーバとかデータベースなどの負荷が減らすことができます。 リバースプロキシキャッシュはキャッシングの一つのタイプであり、Webサーバのクライアントの間に置いておくサーバです。その中で、Varnishは一番よく使われているものです。

f:id:minhquang4334:20211222190744p:plain
Varnishはリバースプロキシキャッシュサーバーである

上記の図を使って、Varnishといったリバースプロキシキャッシュを簡単に説明します。

  • 一度目にクライアントからリクエストを送ったら、VarnishはWebサーバにフォワードします。Webサーバはそのリクエストを処理して、データベースや外部APIと接続してから、レスポンスを生成します。そのレスポンス自体はVarnishのキャッシュに保存されて、クライアントに返します。
  • 2度目以降でクライアントからリクエストを受けたら、すでにレスポンスはVarnishのキャッシュにあるので、すぐ返します。
  • Varnishのキャッシュの有効期限も設定でき、有効期限を超えたら、キャッシュにあるレスポンスは無有効に設定されます。

そのフローでバックエンドコードを変更せずに、負荷が凄く減らせます。この仕組みは特に認証とリアルタイムも必要なくページやデータなどに相応しいと思います。

ISUCON11予選の課題では、ユーザーがログインせずサマリの画面をリロードする度にapi/trendが呼ばれて、かなり重いクエリが発行されてしまいます。そのページが遅くて、ユーザーがサイトに興味がなくなるというビジネスの要件もあります。そこで解説ブログを参考してみてから、Varnishをインストールして0.5sの有効キャッシュ応答をするように変更しました。

  • Varnishを活用する前の該当リクエストのALP
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX  |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |   SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
|  4499 | GET    | /api/trend                   |   0 | 4482 |    0 |    17 |   0 | 0.004 | 0.180 |  542.084 | 0.120 | 0.080 | 0.116 | 0.124 |  0.015 |     0.000 |   5870.000 |  25317465.000 |  5627.354 |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
  • Varnishを活用する後の該当リクエストのALP
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| COUNT | METHOD |             URI              | 1XX | 2XX  | 3XX  |  4XX  | 5XX |  MIN  |  MAX  |   SUM    |  AVG  |  P1   |  P50  |  P99  | STDDEV | MIN(BODY) | MAX(BODY)  |   SUM(BODY)   | AVG(BODY) |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+
| 24461 | GET    | /api/trend                   |   0 | 24454 |   0 |     7 |   0 | 0.000 | 0.020 |    6.500 | 0.000 | 0.000 | 0.000 | 0.000 |  0.001 |      0.000 |   8478.000 | 159572757.000 |   6523.558 |
+-------+--------+------------------------------+-----+------+------+-------+-----+-------+-------+----------+-------+-------+-------+-------+--------+-----------+------------+---------------+-----------+

Varnishを使った後、AVG, P1, P50, P99のレスポンスタイムはほぼ0sぐらいになりました。バックエンドにほぼ手を入れずに、簡単な修正で、信じられないパフォーマンス最適化が出来て、すごく感動しました。今回、Varnishを導入するのが以下となります。

location ~ ^/api/trend$ {
    proxy_set_header Host $http_host;
    proxy_set_header Connection "";
    proxy_http_version 1.1;
    proxy_pass http://varnish;
}
sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.
    set beresp.ttl = 0.5s;
    set beresp.grace = 0.2s;
}

課題の特徴から適切な構成を設計する

ISUCON11予選の課題の特徴として、時系列のデータでありユーザーが増加することにより同時に大量のレコードをデータベースに書き込むといった特性上、どうしてもデータベースの負荷が高くなりパフォーマンスが低下してしまいます。そこで、リクエストごとにデータベースに書き込まずに、書き込むデータをキャッシューに入れ、Scheduled Jobを起動し、定期にキャッシュから取得したデータを一括データベースに書き込むように修正しました。その修正をした後、点数は136226から222174まで急増できました。以下のリンクは今回の修正と測定となります。 https://github.com/minhquang4334/isucon11-training/issues/1#issuecomment-944838242

こちらの改善はパフォーマンスの観点で一番効果が高いと感じています。解決したい課題の特徴を把握して、適切な構成を設計して、実装するのが一番大事かと勉強になりました。それは最初から書いた通りに、正しい課題を解決することだと思います。

終わりに

今回の練習を通じて、Webシステムパフォーマンスチューニングを含めて技術の課題解決についていろいろ勉強になりました。目を通しただけで忘れてしまい、実際にやってみた方がより深く理解できてきたと思っています。その知見を生かして、これからClassiでの業務にも頑張っていきたいです。それとも、今年の予選では敗退してしまいましたが、来年の予選までちゃんと練習して、本選まで挑戦していきたいと思います。💪 💪

自分自身の学んだことや理解したことをこの記事にまとめました。理解が不足している部分もあるかと思います。お気づきの際はぜひコメントしていただきたいです。

明日のClassi developers Advent Calendar 2021の担当は平田哲也さんです。お楽しみに。

トピックモデルを使って問い合わせ内容を分析した話

この記事はClassi developers Advent Calendar 2021の18日目の記事です。
昨日は基盤インフラチームのめるさんによる「バックエンドエンジニアが基盤インフラチームに異動して半年ほど経った話」でした。

こんにちは、データAI部でデータサイエンティストをしている高木です。
弊社では顧客である先生、生徒、保護者からClassiの機能や契約に関する問い合わせを日々頂いております。
これらの問い合わせの内容を分析し、Classiの現状の課題や今後解決していくための施策などを社内で検討しています。

今回は問い合わせ内容を言語処理技術の一つであるトピックモデルを使って分析した内容についてご紹介します。

なぜ分析する必要があったのか?

Classiへの問い合わせやその対応の内容は、担当者によってテキスト化された状態で管理されています。
弊社のカスタマーサポート・カスタマーサクセスチームは、これらの内容を基に次年度の施策を検討したり、実施した施策がどうだったかという効果を分析しています。

年度が変わる3月〜4月での問い合わせが増加傾向にあり、その中でも学校全体の利用に関わる設定・登録の問い合わせが最も多くなっています。
毎年、これらの問い合わせを目視でチェック・分類し、施策を検討している状態だったのですが、作業には膨大な時間と労力がかかってしまいます。

そこで、ある程度自動的に分析・分類できないかという必要性が出てきており、今回トピックモデルを使った分析を試みました。

トピックモデルとは?なぜ使ったのか?

トピックモデルは、文書集合をそれらに含まれる単語の共起性から、どんなトピック(話題や分野など、大体の「意味」のようなもの)が存在するのか、各文書はどのトピックについてのものか、を自動で推定することができます。

これまで様々な手法が提案されており、文書の検索や分類、また、音楽の歌詞探索*1やゲームのデッキアーキタイプの抽出*2など、様々な分野へ応用されています。
私もテスト問題に応用した研究をしています*3

以下は代表的なトピックモデルです。

トピックモデル 特徴 参考文献 参考コード
LSI: Latent Semantic Indexing ・特異値分解による文章の圧縮
・1文書に1トピック
Deerwester et al (1990)*4 Gensim
PLSI: Probabilistic Latent Semantic Indexing ・LSIの確率モデル化
・1文書に複数トピック
Hofmann (1999)*5 PyPI
LDA: Latent Dirichlet Allocation ・PLSIのベイズ化
・新規文書のトピック推定が可能
Blei et al (2003)*6 Gensim
HDP: Hierechical Dirichlet Process ・LDAのノンパラメトリック化
・トピック数が自動決定可能
Teh et al (2006)*7 Gensim
DTM: Dynamic Topic Model ・時系列データへの適用 Blei et al (2006)*8 Gensim
BTM: Biterm Topic Model ・短い文章への適用 Yan et al (2013)*9 GitHub

一方で、問い合わせの内容にはClassiの機能や契約に関する複数の質問が同時に含まれることがあります。
そして、その内容は時期によっても変化します。

そこで今回は、(1)自動で手軽に内容を把握できる、(2)同一の問い合わせから複数のトピックを抽出することができる、(3)内容の時系列変化を追うことができる、 という点を考慮しDTM(Dynamic Topic Model)を使用しました。

下図はDTMによるトピックの推定と時系列変化の例です。
時刻tにおける、ある文書集合のトピックに出現する単語(横軸)と、それらの単語がトピックに出現する確率(縦軸)を表しています。
また、各文書でそれぞれのトピックが出現する確率も推定されます。

f:id:ttakagi1021:20211216200222p:plain
DTMによるトピックの推定と時系列変化のイメージ

DTMによる問い合わせ内容の分析

概要

2020年と2021年の3月〜4月に寄せられた設定・登録についての問い合わせをトピック分析し、内容の変化を考察しました。
DTMを使ったトピック分析の手順と目的は以下になります。
なお、すべての分析はPythonで行いました。

  1. DTMによるトピックの推定
    • 2020年と2021年のそれぞれの問い合わせのトピックとその時系列変化を推定するため
  2. トピックへのラベル付け
    • トピックの内容を把握しやすくするため
  3. トピックごとの問い合わせ件数を集計
    • どのトピックの問い合わせが多いか把握するため
  4. 2020年と2021年のトピック間類似度の計算
    • 類似度の低いトピック(各年の特有なトピック)を抽出するため
  5. ダッシュボードの作成
    • 手順1〜4の結果を可視化するため

分析手順の詳細

1. DTMによるトピックの推定

DTMによるトピックの推定は、Gensimのmodels.ldaseqmodelというライブラリを使用しました。
トピックの推定で使用するメソッドと入力パラメータは以下になります。

dtm_model = LdaSeqModel(corpus, id2word, time_slice, num_topics)
  • corpus:コーパス
    • 文書毎の単語IDとその出現回数を持つタプルリスト
  • id2word:辞書
    • 単語とそれを一意に識別するIDの辞書
  • time_slice:タイムスライス
    • 指定した「期間」ごとの文書数のリスト
  • num_topics:トピック数
    • トピックの数は可変

辞書やコーパスの詳細については省略しますが、これらを作成する際に重要となる単語の抽出処理について後述します。

タイムスライスとは、トピックの変化を指定した「期間」で分割するためのもので、今回はその「期間」を月曜日〜日曜日の1週間ごとにしました。
週毎の問い合わせ件数が以下の場合、time_slice = [3, 5, 1]となります。

  • 2021/03/01(月)〜03/07(日):3件
  • 2021/03/08(月)〜03/14(日):5件
  • 2021/03/15(月)〜03/21(日):1件

さらに、トピック数は分析や分類したい粒度によって手動で設定する必要があります。
今回は以下の理由で両年ともトピック数を10としました。

  • 2020年の問い合わせを調査した際、約10個の重要なカテゴリに分類された
  • 多すぎるとトピックの意味が細かく分散しチェックするのが大変

単語の抽出処理について

トピックモデルではトピックの推定精度を向上させるために、関連のある単語の共起性を高めることが重要になってきます。
一方で、日本語には次のような性質があります。

単名詞Nが対象分野の重要な概念を表しているなら、書き手はNを頻繁に単独で使うのみならず、新規な概念を表す表現としてNを含む複合名詞を作りだすことも多い。*10

この「単名詞N」と「単名詞Nを含む複合名詞」は関連する概念を表していることが多いです。

例えば、Classiのコミュニケーション機能である「校内グループ」では、「グループ」という単語を中心に、「自動作成グループ」、「任意作成グループ」などの機能が存在し、同一の問い合わせ内で出現することが多いです。

この例の場合、「単名詞N」と「単名詞Nを含む複合名詞」は以下になります。

  • 単名詞:校内、グループ、自動、作成、任意
  • 複合名詞:校内グループ、自動作成グループ、任意作成グループ

これらの単語はすべて関連しており、同一のトピックに分類されることが望まれるため、このような単名詞複合名詞をすべて抽出するようにしました。
抽出処理には、形態素解析器のMeCabと形態素解析器の結果を基に複合名詞を抽出することが可能なTermExtractを使用しています。

2. DTMによるトピックの推定

手順1で作成されたdtm_modelにより、特定の期間のトピックに出現する単語と出現確率をリストで出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.print_topic(topic, time, top_terms)
  • topic
    • トピックのID(トピック数10の場合0~9)
  • time
    • タイムスライスで設定した「期間」の順番
  • top_terms
    • 出力する単語の数

このメソッドによる出力結果を用いて、トピックをワードクラウドで可視化しました。
そして、比較的大きく表された単語を基にラベルを付与しました。

最初の週(time=0)のトピックのワードクラウドは以下になります。

f:id:ttakagi1021:20211216184956p:plain
2020年3月 第1週のワードクラウド

f:id:ttakagi1021:20211216185035p:plain
2021年3月第1週のワードクラウド

3. トピックごとの問い合わせ件数集計

dtm_modelでは、問い合わせごとにトピックの出現確率も出力することができます。
そのメソッドと入力パラメータは以下になります。

dtm_model.doc_topics(doc_number)
  • doc_number
    • コーパスや辞書を作成する際に入力した文書の順番

問い合わせごとに最も出現確率の高いトピックをその問い合わせにおけるトピックとし、トピックごとの問い合わせ件数を集計しました。
以下の表は件数の多い順にトピックを並べた結果になります。

2020年3月〜4月

トピックNo ラベル
2 年度更新の各ステップ作業
10 生徒情報の登録
1 生徒や先生の情報登録, ExcelファイルのUPL/DL
6 IDとPW, ログイン, 招待コード, 無償提供
4 管理責任者, 管理者の権限, 生徒カルテ
3 校内グループの設定や配信, 授業の登録
7 学習動画, 学習記録, ダミー生徒
9 継続, 利用停止, 保護者
5 識別番号, 留年や進学した生徒の登録, 人数確定
8 模試データ連携

2021年3月〜4月

トピックNo ラベル
2 生徒情報の登録, ExcelファイルのUPL/DL
3 年度更新
5 年度更新
4 IDとPW, ログイン, 招待コード
8 模試データ連携
7 利用停止, 学習動画パック
6 校内グループ, アンケート
1 管理責任者, 学習動画, 学習記録, 閲覧権限
10 授業登録, 先生登録, 帳票登録
9 人数確定

4. トピック間類似度の計算

手順2で使用したdtm_model.print_topicを用いて、トピックに含まれる単語とその出現確率を50個出力し、2020年と2021年のトピック間の類似度をコサイン類似度で計算しました。

下図はトピック間の類似度をヒートマップによって可視化した結果です。
行が2020年、列が2021年のトピックを示しており、それぞれの類似度の値と値が大きいほど色が濃く表示されています。

例えば、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度がすべて0.5未満と低くなっています。
このことから、topic9は2021年特有なトピックだということが分かります。

f:id:ttakagi1021:20211216190648p:plain
2020年(行)と2021年(列)のトピック間類似度のヒートマップ

5. ダッシュボードによる可視化

手順1〜4の結果をTableauのダッシュボードで可視化しました。

下図は2021年のダッシュボードです。
このダッシュボードで分かることは以下になります。

  • 各年度のトピックを表す単語やその出現確率(図中①)
  • トピックごとの単語の週次変化(図中②)
  • 各トピックに該当する問い合わせ内容(図中③)
  • トピックごとの問い合わせ件数(図中④)
  • トピック間の類似度(手順4のヒートマップを別ページで作成)

f:id:ttakagi1021:20211216201758p:plain
2021年3月〜4月のトピック分析ダッシュボード

内容変化の考察

以上の分析結果を基に、2020年と2021年の内容の変化を考察しました。
ページの都合上詳細は省きますが、いくつか興味深い結果を以下で紹介します。

「年度更新」における各ステップの問い合わせの減少

手順3の問い合わせ件数の多いトピックを見ると、両年ともに次年度のユーザー情報を更新・登録する「年度更新」や「情報登録」についてのトピックが上位3位までに入っています。

2020年はこれらのトピックで「ステップ」や「STEP」という単語を含む問い合わせが多かったのですが、2021年ではそれらの単語を含む問い合わせが減少していました。

この理由としては、2020年から年度更新の手順をステップごとに説明したガイドが作成されたため、ステップごとに問い合わせをする人が増えたと考えられます。

そして、2021年はこれらのガイドを基にした作業を初めて行う先生の数が減少した(作業を理解している先生が増加した)ため、これらの単語を含む問い合わせが減少したと考えられます。

下図は2020年3月〜4月の「年度更新」に関するトピックの推移です。

f:id:ttakagi1021:20211216201916p:plain
2020年の「年度更新」に関するトピックの推移

「利用生徒人数の登録確定」についての問い合わせの増加

手順4で述べたように、2021年のtopic9は同年の他トピックに比べ2020年のトピックとの類似度が低くなっており、2021年特有なトピックとなっています。

このトピックは利用生徒人数の登録確定に関するもので、その作業方法や締切についての問い合わせが多かったです。

この理由としては、2021年は4月に入ってからこれらの確定作業を完了するよう各学校への連絡が増えていました。
その結果、これらの確定作業に関連する問い合わせが増えたと考えられます。

下図は2021年3月〜4月の利用生徒人数の登録確定に関するトピックの推移になります。
4月以降でこのトピックに関連する単語の出現確率が上昇傾向になっているのが分かります。

f:id:ttakagi1021:20211216202022p:plain
2021年の「利用生徒人数の登録確定」に関するトピックの推移

最後に

今回の分析結果から、現状のClassiの設定・登録に関する課題や施策の効果などを考察することができました。
この結果をチームを超えて見ることによって、今年の施策はどうだったのか、今後どういう施策を打っていくべきかを考えることができます。

例えば、カスタマーサポートチームでは今回の分析で、生徒人数の登録確定や年度更新において各設定の期日が分かりづらいことや、年度更新のSTEP3の問い合わせが多いことが明らかになったため、これらの問い合わせを10%減らすことを目標に改善活動を進めています。

また、PMM(プロダクトマーケティングマネージャー)チームでは顧客課題をプロダクトへ反映するために、今回の分析手順をユーザーから寄せられる機能改善についての要望に応用し、その内容の深堀りや要望を実現するための開発の優先順位付けを検討しています。

このように、今回の取り組みが各チームで進められている施策に少しでも寄与できていることは、データAI部としても嬉しい限りです。
分析結果を毎回真摯に聞いて下さりご意見下さる各チームの皆様にはとても感謝しています。

現在、10月までの問い合わせ内容の分析が完了しており、今後は来年の2月までの問い合わせ内容の分析を実施し、年間の問い合わせの傾向を整理していく予定です。

さらに、来年度以降の問い合わせ内容の継続的な分析や、それらの結果を各チームへ素早く還元できる仕組みを構築していきたいと考えています。

明日のClassi developers Advent Calendar 2021の担当は横田さんです。
よろしくお願いします。

*1:Lyric Jumper: https://lyric-jumper.petitlyrics.com

*2:『逆転オセロニア 』における、機械学習モデルを用いたデッキのアーキタイプ抽出とゲーム運用への活用: https://www.slideshare.net/RyoAdachi/deck-archetype-extraction-cedec2019

*3:高木輝彦,高木正則,勅使河原可海,田中健次: e テスティングにおけるLDAを用いた項目間類似度の算出, 情報処理学会論文誌, Vol.55, No.1, pp.91 - 104, 2014.

*4:Deerwester, S., Dumais, S. T., Furnas, G. W., Landauer, T. K. and Harshman, R.: Indexing by Latent Semantic Analysis, Vol. 41, No. 6, pp. 391–407 (1990).

*5:Hofmann, T.: Probabilistic latent semantic indexing, Proceedings of the 22nd annual international ACM SIGIR conference on Research and development in information retrieval, SIGIR ’99, New York, NY, USA, pp. 50–57 (1999).

*6:Blei, D. M., Ng, A. Y. and Jordan, M. I.: Latent dirichlet allocation, J. Mach. Learn. Res., Vol. 3, pp. 993–1022 (2003).

*7:Teh, Y. W., Jordan, M. I., Beal, M. J. and Blei, D. M.: Hierarchical Dirichlet Processes, Journal of the American Statistical Association, Vol. 101, pp. 1566–1581(2006).

*8:Blei D. M. and Lafferty J. D.: Dynamic topic models, Proceedings of the 23rd international conference on Machine learning, pp. 113-120 (2006).

*9:Yan, X., Guo, J., Lan, Y. and Cheng, X.: A biterm topic model for short texts, Proceedings of the 22nd international conference on World Wide Web, pp. 1445–1456 (2013).

*10:中川裕志,湯本紘彰,森 辰則:出現頻度と連接頻度に基づく専門用語抽出,自然言語処理,Vol. 10, No. 1, pp. 27–45 (2003).

UIKitでDesign Systemを実装する

この記事はClassi developers Advent Calendar 2021 の 14日目の記事です。

はじめまして。小中事業開発部でモバイルアプリエンジニアをしています拜郷です。
今回は新規開発中サービスのiOSアプリでDesign System1を実装するにあたって考えたことを書いていきます。

Design System導入の背景

小中事業開発部では現在新規サービス tetoru(テトル) のリリースに向けてチーム開発を行なっています。
開発を進めていく中でUXDチームからDesign System導入の検討がされました。
導入の背景としては開発メンバーが増員するタイミングだったのと、デザインツールの移行が決まったのが大きな要因としてありました。
Design Systemはプロダクト、チームそれぞれの視点で以下の目的を実現できると考えています。

  • プロダクトを通してユーザに一貫性のある体験を提供できる
  • チーム内でデザイン原則を共通認識として持つことができる
  • メンテナビリティとスケーラビリティの向上

iOSアプリ開発の背景

iOSアプリは開発初期の段階からUIKitおよびInterface Builder(以下IB)を扱うStoryboard, xibを使用して開発を行なっていました。
Design System導入のためにIBでのレイアウトをやめたりSwiftUIに変更するのは諸々の事情により現実的ではなかったため、前提として開発方針は変えずDesign Systemの実装検討を進めることになりました。
ただしiOSアプリ開発においてUIKitおよびIBはDesign Systemの実装(主にUIコンポーネントの再利用)に適しているとは言えず少々困難です。
以下ではDesign SystemをUIKitで実装するにあたって考えたことを具体例を交えて紹介していきます。
(※今回紹介するソースコードはすべて本記事用のサンプルコードです。)

デザイントークンの実装

まずDesign Systemにおける最小単位であるデザイントークンの実装を考えます。 デザイントークンにはカラー、余白、行間、Elevation(高さ)、タイポグラフィ、シャドウ、アニメーションなど複数のコンポーネントにまたいで使用される情報を定義します。

カラー

カラーの定義はAsset Catalogを使用します。これはXcode9およびiOS11から使える標準機能なので現時点では特に悩まず使えます。
定義したカラーをコード内で使用する場合、UIColorクラスの初期値にAsset Catalogで定義した文字列を指定します。
ここでSwiftGenR.Swiftを使用することでタイプセーフにリソースを扱えるようになり、タイプミスなどで実行時エラーが発生しなくなる等のメリットがあります。
またAsset CatalogのColorはライト/ダークの各モードをそれぞれ定義することができるため、デザイントークンのカラー定義を双方で検討しておくことでダークモード対応が容易に行えると思います。

// 通常コードからカラーを取得する
UIColor(named: "primary")
// SwiftGenを使用する
Asset.Color.primary.color
// R.Swiftを使用する
R.color.primary()

タイポグラフィ

タイポグラフィの定義はUIFontをextensionして使用することにしました。
SystemFontを使用する場合は単純にトークン名に紐づいたサイズとウェイトを指定して定義します。
カスタムフォントを使用する場合はサイズ、ウェイトに加えフォントファミリーを指定します。ここでもSwiftGenやR.Swiftを使用することでタイプセーフにリソースを扱えます。

public extension UIFont {
    static let title: UIFont = .systemFont(ofSize: 44.0, weight: .bold)
    static let body: UIFont = .systemFont(ofSize: 14.0, weight: .bold)
    static let caption: UIFont = .italicSystemFont(ofSize: 11.0)
    static let button: UIFont = .systemFont(ofSize: 14.0, weight: .regular)
    static let swiftGenSample: UIFont = FontFamily.mplus1.regular.font(size: 14.0)  // SwiftGenでカスタムフォントを指定する
    static let rswiftSample: UIFont = R.font.mplus1pRegular(size: 14.0)!            // R.Swiftでカスタムフォントを指定する
    ...
}

レイアウト要素

余白、角丸、高さなどレイアウト要素の数値を扱うデザイントークンはCGFloatをextensionして定義するようにしました。

public extension CGFloat {
    struct spacing {
        static let xxx_small: CGFloat = 2
        static let xx_small: CGFloat = 4
        static let x_small: CGFloat = 8
        ...
    }

    struct cornerRadius {
        static let small: CGFloat = 2.0
        static let medium: CGFloat = 4.0
        static let large: CGFloat = 8.0
        ...
    }
}

ただしIBからここで定義したレイアウト要素の情報を直接参照できないという課題があります。
この課題に対する1つの解決策として、IB上で設定したNSLayoutConstraintをIBOutletを使ってコードと紐づけるという方法があります。
たとえばある画面をレイアウトする際にSafeAreaとコンポーネントの余白に定義したデザイントークンの数値を使いたい場面があるとします。
この場合IB上では仮の値を設定したNSLayoutConstraintをIBOutletに紐づけます。
あとは紐づけたNSLayoutConstraintにデザイントークンとして定義した値を設定することができます。

f:id:khaigo:20211213101536p:plain
IB上で設定したNSLayoutConstraintをIBOutletでコードと接続する

@IBOutlet weak var btnTrailingConstraints: NSLayoutConstraint! {
    didSet {
        btnTrailingConstraints.constant = <デザイントークンとして定義した値>
    }
}

しかしこの方法はデメリットも存在します。
特に下記のデメリットはIBを使用するメリットを打ち消すので、今回こちらの方法は採用しませんでした。

  • すべての制約をIBOutletに紐づけてコード上で管理するとコード量が肥大化する
  • 動的に制約を書き換えることになるのでStoryboard, xib上のレイアウトと実際に表示されるレイアウトに差異が出る

UIコンポーネントの実装

UIコンポーネントはボタンやフォームなどデザインに使用されるUIパーツを定義します。基本的にはここで定義したコンポーネントの組み合わせで各画面のレイアウトが完成します。
実装方針としてはIBDesignableおよびIBInspectableを使用し、UIKitの各パーツをラップしたクラスを定義しそれをIB上でカスタムクラスとして設定することにしました。
カスタムクラスは定義した各デザイントークンを組み合わせることで共通化を図ります。

またUIコンポーネントを定義する上で、コンポーネントを共通化する単位についてデザイナーとしっかり認識を合わせることが重要になります。
ここでエンジニア、デザイナー間で認識齟齬が起きると以下のような問題が発生するかと思います。

  • 持つべき機能が異なるコンポーネントを共通化してしまう
  • 同じ目的のコンポーネントを別コンポーネントとして切り分けてしまう

いずれもUIコンポーネントが二重管理になったり、想定外の画面でハレーションが起きるなどメンテナビリティ、スケーラビリティを低下させる要因になります。

UILabel

基本方針の通り実装します。カスタムクラスにIBDesignableを付与することでIBから適用したUIが確認できます。
ここでのポイントはUILabelを構成する要素はすべてデザイントークンで定義したものを使用している点です。

@IBDesignable
class TitleLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    private func setupAttributes() {
        // デザイントークンで定義した値を設定する
        font = .title
        textColor = R.color.systemGray_800()
    }
}

@IBDesignable
class BodyLabel: UILabel {
    ...
}

@IBDesignable
class CaptionLabel: UILabel {
    ...
}

定義したUILabelのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213101832p:plain
定義したUILabelのカスタムクラスを設定する

UIButton

こちらも基本方針通りです。
UIButtonの状態に応じてlayoutSubviewsでスタイルを切り替えるようにしています。
その他UIViewを継承するいずれのUIクラス(UITextView, UITextField etc)もこの方針でカスタムクラスを定義してUIコンポーネントを作成するようにします。

@IBDesignable
class FillButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupAttributes()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupAttributes()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setupAttributes()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        switch state {
        case .normal:
            backgroundColor = R.color.brandAccessible()
            titleLabel?.textColor = R.color.systemGray_000()
        case .highlighted:
            backgroundColor = R.color.brandAccessibleActive()
            titleLabel?.textColor = R.color.systemGray_000()
        case .disabled:
            backgroundColor = R.color.buttonDisabled()
            titleLabel?.textColor = R.color.disabled()
        default:
            break
        }
    }
    
    private func setupAttributes() {
        layer.cornerRadius = .cornerRadius.medium
        backgroundColor = R.color.brandAccessible()
        titleLabel?.font = .button
        titleLabel?.textColor = R.color.systemGray_000()
    }
}

@IBDesignable
class CancelButton: UIButton {
    ...
}

@IBDesignable
class TextButton: UIButton {
    ...
}

定義したUIButtonのカスタムクラスを設定することでIB上で反映されたスタイルを確認する事ができます(以下サンプル)。

f:id:khaigo:20211213102016p:plain
定義したUIButtonのカスタムクラスを設定する

まとめ

最後までお読みいただきありがとうございます。
まだまだ諸々の事情によりSwiftUIではなくUIKitを使用するプロジェクトは多々あるかと思います。
実装に関しては基礎的な内容だったかも知れませんがDesign Systemを実装する際の考え方の参考になると幸いです。
私自身もDesign Systemとその実装についてまだまだ模索中ではありますが今後もUnlearn & Learnでやっていきたいです!

Classi developers Advent Calendar 2021 の 15日目はおかじさんです。


  1. Design Systemとは諸説ありますが、大まかに言うとプロダクトのデザインにおける一貫性を効率的に保つための仕組みのことです。
    Design Systemを構成する要素としては、概念・原則をまとめたドキュメント、スタイルガイド、UIコンポーネントライブラリ、またそれらを管理・運用するためのルールやツールなどがあげられます。

© 2020 Classi Corp.