エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

【インターン参加記】フロントエンドからインフラまで全部触ってきました!!!

はじめに

こんにちは、エムスリーで2週間インターンをさせていただいたしーぴー (@__cp20__) です。インターンではWebバックエンドを中心としてWebフロントエンド、インフラ (IaC)、など幅広い分野のタスクをこなすことができました。このブログでは具体的にどんなタスクをこなしたか、それによって何が学べたかなどをご紹介します。

インターン最終日に撮った写真

目次

概要

最初にちょっとだけインターンの一般的な概要を書いておきます。

  • 応募期間: 通年
  • 期間: 2週間
  • 内容: 実際の業務へのコミット (なので部門によって様々)
    • 割と何でもやりたいことはできるみたいです (し、実際できそうでした)

いろいろプロダクトがあるので配属先は様々だと思いますが、割と希望通りにアサインしてもらえるみたいです。ボクは割となんでもできる感じのデジスマチームに配属されました。

もっと詳しいことは↓のサイトの下の方に書いてあるので興味のある方はぜひ目を通してみると良いと思います。

fresh.m3recruit.com

キッカケ

エムスリーのことは元からSNSやブログなどで見かけていてちょっと気になっていました。そんなところで、TSKaigi 2025 で池奥さん (@yuta-ike) と初めて会って「インターン興味ありませんか?」と誘われたのがキッカケで話が始まりました。その後ばんくしさん (@vaaaaanquish) とカジュアル面談を経てインターンに応募する流れになり、いい感じに話が進んだのでインターンに参加できました。

ばんくしさんとの面談の時に「エムスリーはいろいろやってるから割となんでもできるよ」という話を聞いていたのですが、その通りで本当にいろいろなことを経験できました。という話をこれからしていきます。

タスク1: 請求画面の速度改善

最初にパフォーマンス改善のタスクをもらいました。三度の飯とパフォーマンスチューニングが同じぐらい好きなので、嬉しいです。

解決策の検討

早速何が遅いのかの分析から始めました。大まかにコードを読んだ結果DBへのクエリが遅いのではという仮説を立てました。クエリ内で多数の集計や演算を行っており、インデックスはある程度効いてはいるものの、APIのレスポンスとして許容できるかというと微妙な速度であることを EXPLAIN ANALYZE などを用いて確かめました。

改善方法について制約はなく、そもそもの画面の仕様の見直し含めて何をやっても良いという話でしたが、いろいろ検討した結果 Materialized View を使って改善しました。インターンの中間発表の際にこの「いろいろ検討した結果」にいくつか突っ込まれたので、参考としてここにも書いておこうと思います。

解決案1) 画面に表示する行数を制限する (現状全期間取得しているものを1年ごと、月ごとなどに分ける)

前提として、現状でも1医療機関に対して大量の請求が発行されています。これは今後も増加していくので、単純に期間を分割するだけではパフォーマンス問題は解決できないと考えました。(緩和することはできますが) さらに請求の少ない医療機関では不要な操作が発生する可能性があり、ユーザー体験が悪化する可能性も考えられます。

解決案2) クエリを改善する (インデックスを貼る、構造を変える)

EXPLAIN ANALYZE の結果を見て考えた結果、インデックスやクエリの書き換えによって得られるパフォーマンスはそこまで大きくないと考えました。また、パフォーマンスを求めて無理に大きいインデックスを作ることはwrite操作のパフォーマンスを悪化させる可能性があります。

解決案3) DB を非正規化する

どうしても重いクエリのチューニングの裏ワザとして知られているDBの非正規化ですが、もちろんあまりやりたくはありません。今回のような集計クエリ (あまり叩かれないが、1クエリが重い) の場合は非正規化というより実行結果をキャッシュしておくことの方が有用でなので、検討しませんでした。

解決案4) writable なキャッシュを使う

請求などの情報を集計した値なので、事前に集計した上で情報が変わった部分だけ適宜再計算したキャッシュを用意するというやり方です。リクエスト時には単にキャッシュを返すだけで良く、パフォーマンスを大幅に向上させることができます。しかしあまり頻繁にリクエストされるわけではないので不要な再計算が多く走ってしまう、集計元の情報が変更される場所全てで適切な再計算ロジックを書く必要があり実装工数が大幅にかかる、という理由で採用しませんでした。

解決案5) read-only なキャッシュを使う

これが今回採った解決策です。writable なキャッシュとやることは大きく変わりませんが、read-only という名の通り適宜再計算するということをしません。あくまで元々の遅い集計クエリを走らせてキャッシュしておくだけなので、あまり頻繁に更新できず、集計データと実際のデータが最大で更新間隔分ズレてしまうというデメリットがあります。しかし writable なキャッシュに比べて圧倒的に実装がシンプルになるので、ある程度の集計ラグを許容できるならかなり良い選択肢だと思います。

read-only なキャッシュにも Redis やオンメモリキャッシュなどいろいろ選択肢が考えられるのですが、実装コストが低い Materialized View を選択しました。元々の集計クエリを Materialized View に移して、定期的な更新クエリの発行だけ書けば良いだけなので実装はかなりシンプルで分かりやすいです。

中間発表の際に「Materialized View はテーブルとしての性質を持っているので、柔軟に絞り込み・並び替えなどを行える一方で、クエリ速度が他のキャッシュと比べると劣る」というデメリットを教えていただきました。そういう点では単純な Key-Value ストア (KVS) の方が優位に立てるということですね。そういう意味で集計クエリはそこまで頻繁に叩かれるものでもないと思うので、Materialized View 向きですね。逆に大量に叩かれる簡単なクエリ (例えば id からユーザーを引いてくるクエリ) のような場面で Redis などの KVS が役に立ちそうですね。

実装の際に REFRESH MATERIALIZED VIEWCONCURRENTLY オプションを初めて知りました。実はデジスマチームで Materialized View の採用は初めてだったのでO/Rマッパ周りのコードも書いたのですが、そこでの設計で結構 PostgreSQL 公式ドキュメントにお世話になりました。(Kotlin Exposed が Materialized View をサポートしてくれれば話は早いんですが、、、)

www.postgresql.jp

改善効果の測定

改善前は最も請求の多い医療機関で 4000ms (4秒) 程度かかっていたAPIリクエストが、わずか 35ms (0.035秒) 程度まで削減されました!! 事前計算しているものを返すだけなのでこれぐらい速くなるのは当たり前なのですが、ロードに時間がかかっている画面だったのが、あまりに速すぎてAPIリクエストが走っていることを実感することすらできない画面に変貌させることができました。素晴らしい。

ちなみに定期的に処理している REFRESH MATERIALIZED VIEW も25秒程度で完了するので、結果としてかなり良い解決策を取れたと感じています。

タスク2: ログイン導線の改善

次はフロントエンド周りのタスクをもらいました。デジスマは利用する患者様に認証をしてもらっていますが、医療機関のページなどから初めて予約する時に限り、スムーズに予約を完了するために一時的に認証をスキップして使い始められるようになっています。予約をとった後にユーザの都合の良いタイミングで認証をしてもらうのですが、認証のことを忘れていたまま来院されてしまうとスムーズに自動決済機能を活用できなくなってしまいます。

そこで、自動決済をメインで使われている施設を予約される際には、初めから認証スキップを表示しないことで全体的な患者様と利用施設様の体験を設計できる変更を加えることになりました。 改善策は単純で、予約しようとしている診療科がデジスマ払いのみに設定している場合は認証スキップの導線を提示しなければ良いです。

これだけ聞くとフロントエンドの改修だけで終わりそうだな、と思っていたのですが、実はいろいろあってAPIに機能追加をする必要があり、思ったより実装に時間がかかりました。 それはそれとして実装が終わって、レビューも通って、QA (リリース前の機能チェック) の段になってちょっと動作がおかしいかも?というフィードバックを頂きました。が、なかなか再現できず、QAでも発生頻度が低い事象だったこと、仮に認証スキップが出たままでもリリース前の仕様と同等で運用悪化しないことからリリース判断となりました。

幸いQA環境での再現条件も分かり、最終的にはちゃんと実装することができて、無事リリースまで見届けることができました。嬉しいです😆

タスク3: 請求画面のソート順序の整理

タスク1を実装していた際に上がった問題です。ソート順序があまり直感的じゃないのでは?という意見をレビュー中にもらっていたので、別のタスクとして切り分けて進めていました。

一部のソート順を逆にするだけなのでサクッと実装できるかなと思っていたんですが、開発チームと改めて検討したところAPIの改修が必要という判断に至りました。

マイクロサービスにしている都合上、マイクロサービス間の通信にも一定の互換性を保つ必要があり、それもまたAPIの改修を難しくする要因になりました。

さらに悪いことに、APIの改修中に openapi-generator のバグを踏んでしまい(!!)その解決にも結構時間を取られてしまいました。苦しかったです。

github.com

流石にパッと直せる系のバグではなく、インターン期間中に openapi-generator 自体を直すのは諦めてしまいました。

何はともあれ、インターン期間内にQAまでは漕ぎ着けることができたので良かったです。

タスク4: マイクロサービスごとの Redis を統合

バックエンドがマイクロサービスとして分割されており、現状ではそれに従うように異なるマイクロサービスでは異なる Redis のインスタンスが利用されていました。AWS の価格設定ではインスタンスが複数存在するとその分コストがかかってしまうので、これを1つにまとめることでコストを削減するというタスクです。もっと言えば、より安く利用できる Valkey に移行することでさらなるコスト削減を目指すことも視野に入れていました。

Redis と Valkey の互換性

最初に Valkey 移行の可能性を考えるため、Redis と Valkey がどれだけ互換性を持つのかを調べてみました。案の定目立った互換性の問題はなく、現状利用している Redis OSS v7 からは問題なくアップデートできそうだという結論に至りました。AWS のドキュメントや Valkey のリリースノートなどを参考にしたのですが、AWS からコスト削減のためにユーザーに Valkey を使ってほしいという気持ちをひしひしと感じました。

Valkey は、Redis OSS 7 のドロップイン代替として設計されています。コンソール、API、または CLI を使用して Redis OSS から Valkey にアップグレードするには、新しいエンジンとメジャーエンジンのバージョンを指定します。エンドポイント IP アドレスとアプリケーションの他のすべての側面は、アップグレードによって変更されません。Redis OSS 5.0.6 以降からアップグレードしても、ダウンタイムは発生しません。

docs.aws.amazon.com

Redis から Valkey にシームレスにアップデートするボタンがあったので、単に Valkey に移行するだけならそれを使うだけで良いと思います。が、今回はインスタンスの統合が必要だったのでもう少し複雑な手順を踏むことにしました。

インスタンスの移行 (統合)

デジスマにおいて Redis は単なるキャッシュレイヤーであって、最悪全部のデータが飛んでも問題ないのですが、急に全部のキャッシュが飛ぶと Cache Stampede (Thundering Herd) 問題を引き起こします。これは今までキャッシュを参照していたクライアントが一斉にオリジンに問い合わせを行い、オリジンの負荷が急激に上昇する問題のことです。いろいろ対策はあるのですが、この場合はキャッシュを飛ばさない、つまりデータを適切に移行するという解決策を取ることにしました。

データを適切に移行しつつインスタンスを移行するために次のような計画を立てました。(最大のTTLは24時間です)

  1. Redis (read/write) ← 現状
  2. Redis (read/write) + Valkey (write) ← write だけ多重化 (何も問題は起こらないはず)
  3. (Redis と Valkey のデータが同じになるまで待つ)
  4. Redis (write) + Valkey (read/write) ← read を Valkey に切り替え (問題が起これば Redis にロールバックする)
  5. Valkey (read/write) ← Redis を消す
  6. (移行完了)

他のチームメンバーからも良さそうというレビューを貰ったので、この方針で進めることにしました。

移行の途中にQA環境を二重の要因で破壊するという事件は発生したものの、本番環境の移行自体は問題なく進めることができました。進めるのが遅かったために最終的に本番環境の Redis を消すところまでは辿り着けなかったのですが、QA環境まではたどり着いたので良かったです。リリース時にバグらせないことを祈ります。

インターンを振り返って

2週間という短い期間ではありましたが、非常に刺激的な体験だったと思います。いくつか観点を分けて振り返りをしてみます。

良かったこと: いろいろなコードが触れた

今回フロントエンド、バックエンド、API定義、DBスキーマ、インフラ (IaC) とかなり広い分野にまたがってタスクをこなすことができました。特にインフラ周りはインターンで経験するのは初めてだったので、良い経験になりました。

その中で感じたのが、それぞれリポジトリが分かれているとリポジトリに跨った修正をしようとした時に結構めんどくさくなるな、ということです。ただ今はそれを直すべく、バックエンドの各マイクロサービスごとに散らばっていたリポジトリを1つに、DBスキーマをバックエンドに統合、API定義をバックエンドに統合、といったことをやっている真っ只中でした。(現にインターン期間中にDBスキーマは統合されました) ボクはmonorepoの方が好きなので、喜ばしいことだと思って見ています。(が、もうインターンは終わりなのであまり関係はない、、、)

良かったこと: アウトプットを出せた

いろんなコードを触れた、という部分と関連していますが、ちゃんと本番に影響を与える成果をいくつか出せたのは良かったと思っています。

バリバリにできたでしょうか?

もう少し頑張りたかったこと: QA環境を壊した

タスク4の作業の途中で二重のミスによってQA環境を破壊するというミスをしてしまいました。本番環境ではないので問題ないと言えば問題ないのですが、困ると言えば困るのでなるべく起こしたくないミスです。ちゃんとdev環境で動作確認をしていれば少なくとも片方のミスは防げたはずなので、ボクの雑さが浮き彫りになってしまった形です。

インフラ作業は慎重さが求められるところなので、ちゃんとサンドボックス的な環境で動くことを確認するのが大事ですね。サークルのインフラ管理でも度々アホな操作をしてインフラを破壊してしまっているので、ちゃんとしていきたいところです。

おまけ: 数字で振り返るインターン

マージしたPR21件、コミット数173件、変更した行数4892行
よくがんばりました

業務以外の話

リモート勤務

初日と最終日だけ出社で、それ以外は自宅からリモートで働いていました。始業が9:00ということになっていたので、朝弱い系エンジニアのボクにとってはリモートワークはありがたい環境だったのかなと思っています (それでも早いけど!!) 他のチームメンバーもほとんどリモートなので特にリモートだから何か困るということはなかったです。ただ最初の方は権限周りとかでいろいろ問題を起こしており、たびたびメンションを飛ばしていました。すぐに対応していただいてありがとうございますという気持ちでいっぱいです。

M3 Tech Talk

2週間に1回ぐらい開催されている M3 Tech Talk で発表させていただく機会をもらいました。インターンの中間発表と同じ日だったので、同じようなことを喋っても良いんですが、せっかくなので個人的に最近面白いと思っているコンパイラの最適化についての話をさせてもらいました。そんなに専門的じゃなくてマサカリが投げられないかヒヤヒヤしていたんですが、概ね好評そうな感触でよかったです。せっかくなのでスライドをここに置いておきますね。

www.docswell.com

もしかしたらそのうち YouTube に公開されるかもしれません。

他のインターンと比べてみて

最終発表の時にも聞かれたので、ここにも少し書き残しておくことにします。

毎日KPIをみんなで見る

毎日の朝会の冒頭でKPIをみんなで確認します。ボクの経験した他のインターン先ではここまでちゃんとKPIを確認することはなかったので、ちょっと新鮮でした。いろいろ意味はあるんだと思いますが、毎日確認するとかなりKPIを意識するので、ちゃんと顧客目線でプロダクトを開発できるのかな、という気がします。

1チャンネルに全てを詰め込む

これはエムスリーの文化というよりはデジスマの文化らしいのですが、1チャンネルに業務上の会話も、GitHubのログも、Alertも全て集約しています。その方が便利だよねということらしいですが、確かに検索とかには便利な気がします。

機能単位でまとめてアサインする

これはフロントエンド・バックエンド・インフラといった分割をせずに、1つの機能については1人のエンジニアが基本全ての分野について実装するというやり方のことを指しています。特に大きいプロダクトだとそれぞれチームを分けて、同じ機能についてもまずバックエンドを誰かが実装して、それが終わってからフロントエンドの人が実装する、みたいな分業がよく行われていると思います。1人でやった方がコミュニケーションコストを抑えることができてスムーズに実装できる一方で、純粋に1人のエンジニアの負担が増えるという問題が考えられます。どちらのやり方が良いというわけではないですが、ボクは機能単位で1人でやる方が気楽で良いな、と思います。

さいごに

いろいろ詰め込んだらそこそこの分量のブログになってしまいました。2週間しかインターンしてないのに。

短い間でしたが、本当にお世話になりました!! デジスマチームの皆さん、その他関わってくださった皆さん、本当にありがとうございました!!

We are Hiring!

デジスマチームでは通年でインターンを募集しています。興味を持った方はぜひ応募してみてください!

fresh.m3recruit.com