RailsのZeitwerk化でやったこと、見落としていたこと

Railsアプリケーションで、`classic`オートローダーを使っているものがあったのですが、Rails v7からはZeitwerkしか使えなくなってしまうのでZeitwerkへ移行しました。

手順は基本的にはRailsガイドに乗っているのですが、やりながら詰まったところ等もあったので記事にまとめました。

https://railsguides.jp/classic_to_zeitwerk_howto.html

## やったこと

やったことは大きく、以下3点です。

1. autoloaderを`:zeitwerk`に変更して、`bin/rails zeitwerk:check`でエラーが出た部分を修正
2. 不要な自動読み込みを削除
3. アプリケーション起動時の自動読み込みの修正

1つ目は比較的すぐ完了しましたが、2と3については少し時間がかかった部分もありました。

### 1. autoloaderを`:zeitwerk`に変更して、`bin/rails zeitwerk:check`でエラーが出た部分を修正

`bin/rails zeitwerk:check`でチェックができるのですが、このときエラーが出たのは主に略語の扱いでした。

> classicオートローダーは、すべて大文字のVATを自動読み込みできます。その理由は、オートローダーの入力にconst_missingの定数名が使われるからです。VATという定数に対してunderscoreが呼び出されてvatが生成され、これを元にvat.rbというファイルを検索することで、ファイルが正常に見つかります。
>
> 新しいZeitwerkオートローダーの入力はファイルシステムです。vat.rbというファイルがあると、Zeitwerkはvatに対してcamelizeを呼び出し、冒頭のみが大文字のVatが生成されます。これにより、Vatという定数名が定義されていることが期待されます。以上がエラーメッセージの内容です。

https://railsguides.jp/classic_to_zeitwerk_howto.html#%E7%95%A5%E8%AA%9E%E3%81%AE%E6%89%B1%E3%81%84

全て大文字の略語が使われているクラスがいくつかあったので、都度判断して修正しました。

### 2. 不要な自動読み込みを削除

自動読み込みパスに入っていながら、本番環境で使っていないコードがありました。
全て精査する必要はないとは思いますが、今回「development以外の環境でのみ」エラーが発生していました。

原因としては、自動読み込みパスにあるファイルから、developmentでしかインストールしていないgemを `require` していたためです。

対応としては、

1. 対象のファイルを `Rails.autoloaders.main.ignore` で自動読み込みパスから除外する
2. development等で実行しているファイルから `require` で個別読み込みする

の順で行いました。

ちなみに、このファイルは`lib`配下にあり、今までは`lib`を全て自動読み込みパスに入れていたのですが、この場合Rails v7.1からは`config.autoload_lib`を使うことでシンプルな設定ができます。

https://qiita.com/kyntk/items/b7617e9eb959c44ecf16

:::note warn
このように、本番環境でのみエラーが発生する可能性があるので、`RAILS_ENV=production`等の環境でも`zeitwerk:check`をしておくのをおすすめします
:::

### 3. アプリケーション起動時の自動読み込みの修正

2までで、アプリケーションを起動してもエラーは発生しなくなりました。しかし、エラーは出ないものの、意図しない挙動が発生している可能性があります。

特に、アプリケーション起動時に実行する`config/initializers`の設定から、自動読み込みパスにあるファイルを読み込む時に注意が必要です。

https://railsguides.jp/autoloading_and_reloading_constants.html#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E8%B5%B7%E5%8B%95%E6%99%82%E3%81%AE%E8%87%AA%E5%8B%95%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF

具体的には、アプリケーション起動時に、以下の点への影響確認が必要です。

- (1) 自動読み込みパスにあるファイルを自動読み込みすると deprecation warning が表示される
- (2) 再読み込み可能なクラスへの設定等の処理は、起動後に設定がリセットされる
- (3) 再読み込みが有効な場合、起動時に参照したクラスが更新されても、起動時におこなった設定は古いキャッシュを参照してしまう

#### (1) 自動読み込みパスにあるファイルを自動読み込みすると deprecation warning が表示される

`log/development.log` に以下のような DEPRECATION WARNING が表示されていました。

```md
DEPRECATION WARNING: Initialization autoloaded the constants XxxYyyZzz.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload XxxYyyZzz, for example,
the expected changes won't be reflected in that stale Class object.

These autoloaded constants have been unloaded.

In order to autoload safely at boot time, please wrap your code in a reloader
callback this way:

    Rails.application.reloader.to_prepare do
      # Autoload classes and modules needed at boot time here.
    end

That block runs when the application boots, and every time there is a reload.
For historical reasons, it may run twice, so it has to be idempotent.

Check the "Autoloading and Reloading Constants" guide to learn more about how
Rails autoloads and reloads.
```

最初の3行に以下のようにあります。

> DEPRECATION WARNING: Initialization autoloaded the constants XxxYyyZzz.
>
> Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

以下の記事でも説明されていますが、アプリケーション起動時 (`config/initializers`等) に自動読み込みをしようとしたとき、Zeitwerkはまだ起動しておらず、今後エラーが発生する可能性があります。

https://qiita.com/fursich/items/717a720d9f4465e4cbbb#8-configinitializers%E3%81%A7%E5%AE%9A%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%86

そのため、読み込みが必要なものを明示的に `require` で読み込む対応を行いました。

#### (2) 再読み込み可能なクラスへの設定等の処理は、起動後に設定がリセットされる

結論から言うと、この点について今回対応は不要でしたが、記載します。

Railsガイドでいうと、 [9.1 ユースケース1: 起動中に、再読み込み可能なコードを読み込む](https://railsguides.jp/autoloading_and_reloading_constants.html#%E3%83%A6%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%82%B91-%E8%B5%B7%E5%8B%95%E4%B8%AD%E3%81%AB%E3%80%81%E5%86%8D%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80) に記載されています。

再読み込みは `config.enable_reloading` で設定されています。
再読み込みが有効な場合（主にdevelopment環境）、クラスが再読み込みされる際にクラスの`unload`が行われるため、起動時に以下のような設定を設定していたとしても設定がリセットされてしまいます。 
(Railsガイドからサンプルを引用)

```rb
ApiGateway.endpoint = "https://example.com"
```

再読み込み後にも適用したい場合は、`Rails.application.config.to_prepare`や`Rails.application.config.after_initialize`を使う必要があるようです。

#### (3) 再読み込みが有効な場合、起動時に参照したクラスが更新されても、起動時におこなった設定は古いキャッシュを参照してしまう

例えば、以下のように、再読み込みされないオブジェクトに、再読み込み可能なクラスを使うことがあります。

```rb
# HogeMiddleware は再読み込み可能
Rails.application.config.middleware.use HogeMiddleware
```

この場合、`HogeMiddleware`が再読み込みされたとしても、`config.middleware`に設定されたものは再読み込みする前の古いキャッシュを参照するので、混乱の元となります。

このように、再読み込みされないオブジェクトへ設定するものは再読み込みの対象外とするのが良いです。
具体的には、

- `Rails.autoloaders.main.ignore`等で対象のファイルを自動読み込みの対象外にした上で `require` する
- `config.autoload_once_paths`で再読み込みせずに自動読み込みをする

のような方法があります。

今回は自動読み込みする必要がなかったので、前者の方法で対応を行いました。

## これで完了

以上を行うことで、問題なくリリースすることができました。
実は、Zeitwerk化は3年前にも一度試したものの、断念した形跡があったので、3年越しのリリースとなってしまいました。

個人的には、開発環境以外でのみ読み込みエラーが発生する件や、`log/development.log`にwarningが出ている件は見落としがちだと思ったので、他に対応される方がいればご注意ください。

## 参考

https://qiita.com/fursich/items/717a720d9f4465e4cbbb

https://product.st.inc/entry/2021/08/13/105437

https://railsguides.jp/autoloading_and_reloading_constants.html

https://railsguides.jp/classic_to_zeitwerk_howto.html