ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

ChatGPT での大規模リファクタリング - 理想と現実

※ こちらは ミツモアAdvent Calendar 2023 の 2 日目の記事です。

こんにちはミツモアで VPoE をしている白柳(@yanaemon)です。

Generative AI、特にミツモアでは Github Copilot & ChatGPT を利用していますが、小規模なリファクタリングはとても効果を発揮します。

最近では、Github Copilot や ChatGPT に普段から頼りまくりです。

大規模なリポジトリを読み込ませて、自動的にリファクタリングをできるととても理想ですが、やはりまだ精度を高く実施できるというほどではありません。

そこで、今回は ChatGPT の力を借りて 100 以上に及ぶ機能群を持つ Nest.js アプリケーションの構成変更を大規模リファクタリングするアプローチを 1 つを紹介したいと思います。

背景 : Nest.js の Module 化対応

ミツモアのメインのアプリケーションでは、Nest.js を利用しておりますが、過去の構成の名残で、component ごとにディレクトリを分けていました。

├── app.module.ts
├── controllers
│   ├── requests.ts
│   └── users.ts
├── repositories
│   ├── requests.ts
│   └── users.ts
└── services
    ├── requests.ts
    └── users.ts

この構成は、各種 component ごとのリファクタリングはしやすい一方で、プロダクトの規模が拡大し、機能が増えてきたときにディレクトリ内のファイルが増加し、また app.module.ts に Component が全て import されている状況だったため、依存関係を把握しにくい状況になっていました。

MeetsOne の開発も行っているため、将来に共通機能を切り出しやすいように module ごとにまとめていく方針に切り替えました。

└── modules
    ├── requests
    │   └── requests.controller.ts
    └── users
        ├── users.controller.ts
        ├── users.module.ts
        ├── users.repository.ts
        └── users.service.ts

参考 docs.nestjs.com

移行自体は下記のようなステップです。

  1. 機能ごとに module ディレクトリと module ファイルを作成
  2. component ファイルをディレクトリ移動し、機能ごとの module に登録していく
  3. app.module から component は削除し、module import に切り替える

移行自体はシンプルですが、何せ数がとても多いため、手動で移行していくには多大なコストを要します。

1, 2 のステップについては比較的簡単に実施できますが、3 のステップでは、依存関係をこの機会に整理もしたく、不要な export / import はしたくない!という思いがありました。

そこで、特にステップ 3 では単純に移行するのではなく、依存を特定しつつリファクタリングするために、Tool や ChatGPT の力を借りられないかと進めました。

しかし、現状では全てを ChatGPT で対応するのは厳しいと感じていたため、リファクタリングするためのコードを自動生成するアプローチを取りました。

移行対応の詳細

1. 機能ごとに module ディレクトリと module ファイルを作成

Nest.js には schematics という、雛形生成、いわゆる scaffold の仕組みが提供されています。

docs.nestjs.com

基本的にやりたいことは schematics で実現可能なのですが、ミツモアのアプリケーションの構成とは微妙に異なるためそのまま使うと、ディレクトリ構成が Nest が深い状態なので、そのままだとディレクトリの移動が必要になりました。

もともと、ミツモア独自の component を自動生成するため、独自の schematics を作成する仕組みがあったため、それを流用して自動的に生成できるようにしました。

独自の schematics の作成方法については下記を参考にさせていただきました。この記事では詳細は詳細は割愛させていただきます。

blog.morifuji-is.ninja

2. Component ファイルをディレクトリ移動し、機能ごとの module に登録していく

ファイルのディレクトリ移動に関しては、対象のファイルを git mv していくだけだったので、git mv コマンドを先に作って一括実行で Done です。

# controllers/requests.ts
# controllers/users.ts
$ for f in `ls -1 controllers`; do; git mv controllers/$f modules/${f/.ts/}/$f; done

# Output
$ git mv controllers/requests.ts modules/requests/requests.ts
$ git mv controllers/users.ts modules/users/users.ts

3. app.module から component は削除し、module import に切り替える

実施方法としては、Nest component の依存を確認し、以下の条件で module に登録することです。

  • module の export は最小限にする(不要な export をしない)
  • 他の module に依存している component は module 経由で import する
    • module 経由にしない場合、import 先の module の依存関係が増えた場合、その依存を利用元の module の providers にも追加しなければならなくなるため

例えば RequestsModule が UsersService は利用しているが、UsersRepository は利用していない場合

// users.modules.ts
// ❌
@Module({
  // 全て export しない
  exports: [ UsersService, UsersRepository ],
})
// ⭕
@Module({
  exports: [ UsersService ],
})

// requests.module.ts
// ❌
@Module({
  // 直接 import しない
  // 例えば UsersRespository に HogeRepository の依存が増えた場合、
  // RequestsModule の providers にも HogeRepository を追加しないといけなくなるため
  providers: [ UsersRepository ],
})
// ⭕
@Module({
  // import は module 経由で
  imports: [ UsersModule ],
})

実施したいことはとてもシンプルで、ChatGPT が大活躍できそうですが、

現時点で Repository 全体を読み込み適切にリファクタリングしてもらうのはまだ難しいなと感じております。

実際生成されたコードが期待通りでない場合、結局手動で直したり、再生成したりと完璧でないことも事実です。

一方で、過去にを利用してリポジトリを一括リファクタリングしたこともあり知見がある状態です。

過去の記事はこちらを参照ください。 engineering.meetsmore.com

やはり普段から使っているわけではないため、ドキュメントを見ながら書くのは時間がかかります。

そこで、ChatGPT にリファクタリングのための Script を書いてもらい、その Script で一括置換する方針を取りました。

下記は ChatGPT に入力した最初の Prompt です。

今回は使用するツールとして、知見もあった ts-morph を指定しています。

ts-morph を利用して、下記の replace をするための script を TypeScript で記述してください。
- 引数に下記を取る
    - Nest.js module が存在する directory
    - Nest.js の module 名
- 処理の流れ
    - 指定された directory に存在する file を再帰的に検索
    - Nest.js component を探す
    - Nest.js component が依存する component を洗い出す
    - Nest.js component が依存する component 自身の directory に存在する component か判定
        - 存在する場合 : controller なら module の controllers, それ以外は providers に登録する
        - 存在しない場合 :  import path "modules/<module 名>" から module 名を特定し、 module を imports に登録する
    - 外部で利用されているかどうかを判定し、利用されている場合 exports に登録する
- すでに登録済みの components は登録しない

実行、エラーや想定外の出力でうまくいかない場合も、ChatGPT にその内容を伝えることで修正でき、最終的に多少の手直しくらいで、爆速で目的を達成できました。

実際このような形で実行すると、対応する module (この例だと users.module.ts)の依存を現在の依存を解析して正しく登録できるようになりました。

$ ts-node updateNestModule.ts --key users

移行する際の特に確認すべき事項としては、Component が漏れている場合はアプリケーション自体が起動しないためすぐ気づけますが、Controller の登録漏れは最も注意すべき事項です。

起動時の Nest.js の Entry Point の登録ログを比較をし、Controller の登録漏れがないことを確認しました。

変更は全て自動で行いましたが、特殊な処理をしている機能があったのでそこだけ手直ししたり、コードレビューやデグレチェックなどはもちろん実施しました。

また、他の業務もあったのでそれも進めながら、それでも 1 週間ほどで事故なく一気に入れ替えることができました!

まとめ

Generative AI だとまだ大規模なリファクタリングは難しいなと感じている場合に、リファクタリングするためのコードを自動生成するアプローチで安全に一括置換した話をしました。

Generative AI で小さな規模のリファクタリングならうまくいくけど、大規模なリファクタリングはなかなか難しいなと感じている方はぜひ試してみてください。

ミツモアでは様々な職種のエンジニアを積極的に採用しています! ご興味がある方はぜひ気軽に面談しましょう!