『ソフトウェアアーキテクチャの基礎』読書メモ・感想

年末年始に、そのボリュームの多さになかなか手がつけられていなかった『ソフトウェアアーキテクチャの基礎 -エンジニアリングに基づく体系的アプローチ』を読みましたので、自分なりのまとめと感想を記事にしました。

www.oreilly.co.jp

どんな本?

  • 現代のソフトウェアアーキテクチャのバリエーションやトレンド、それぞれの特徴について解説しつつ、ソフトウェアアーキテクトという職業に求められるスキルなどについても解説している本
  • いわゆるコードレベルの設計ではなく、例えばマイクロサービスとか、モノリシックなアーキテクチャとか、そういう次元の話
  • 自分は本書でいう「アーキテクト」の仕事はしたことはなく、「開発者」の立場として読んだが、興味深かった
  • あまり知らない領域の話だったものの、丁寧に解説されていて、大事なことは何度も繰り返し主張されるので、意外と理解に困ることはなかった

各章の自分的まとめ・感想

2章「アーキテクチャ思考」

  • アーキテクトは、技術的な深さよりも幅が求められ、そのための努力が必要
  • アーキテクチャ決定は必ずトレードオフであることを意識する必要がある

3章「モジュール性」

  • 計算式は理解が難しくて完全にはわからなかった
  • 適切な抽象化やコナーセンスの度合いを抑えることで変更につよいシステムを構築することが必要

qiita.com

4章「アーキテクチャ特性」

  • いわゆる「非機能要件」の言い換え的な話
  • 色々な特性(信頼性、拡張性、etc)があるが、全てを取るのは不可能であり、どの特性が必要かを選択する必要がある
  • 詰まるところ、これもトレードオフの関係になるし、イテレーティブな設計になっていればやり直しがしやすくなる

5章「アーキテクチャ特性を明らかにする」

  • ドメインの関心事をアーキテクチャ特性として翻訳する
    • この際に、短絡的にならないように注意。
    • 例えば、市場投入までの時間というドメインの関心事に対して、「アジリティ」というアーキテクチャ特性だけに翻訳してしまうと、テスト容易性やデプロイ容易性の観点を損ない、目的が達成できない可能性がある
  • アーキテクチャ特性の優先順位を決めすぎず、上位3を決める、というような方法にする
    • どのアーキテクチャ特性が重要ではないかを決めるのは必要だが、一番必要なのはどれか、というところは不毛な議論になりやすい
  • アーキテクチャアーキテクチャ特性を満たさなくても、アプリケーションの設計で満たすことができる可能性もある

6章「アーキテクチャ特性の計測と統制」

  • アーキテクチャ特性を計測する仕組みを作ることが必要
  • また、必要と決めたアーキテクチャ特性については、適応度関数の仕組みなどを入れて統制させる仕組みづくりが必要

7章「アーキテクチャ特性のスコープ」

  • アーキテクチャ特性について、以前はシステム全体に対する特性、だったが、現代の開発では、マイクロサービスの登場などにより、もっと細かい単位でスコープの設定をするべき状況になっている
  • その単位を著者らは「アーキテクチャ量子」と定義している
  • 本章では、オークションサイトのアーキテクチャ例を紹介しながら、例えば入札者と競売人で必要なアーキテクチャ特性が異なることなどを説明している

8章「コンポーネントベース思考」

  • モジュールをパッケージ化した「コンポーネント」を発見することが必要
  • そのためには、アーキテクチャをどのように分割するか(最上位の分割)を決める。
    • これには、ドメインによる分割や、技術による分割などの手法がある

10章「レイヤードアーキテクチャー」

  • シンプルで親しみやすく、コストが低い
  • アーキテクチャが固まっていない時点の出発点としても適切になることが多い
  • 一方で、システム単位が大きくなるので、デプロイ容易性や、弾力性、耐障害性(どこかでメモリ不足になると全体がクラッシュ)が低いなどのデメリットもある

11章「パイプラインアーキテクチャ

  • bashzshの基本原則になっているようなアーキテクチャ
  • 特にシンプルな一方通行の処理を数sめるタスクで用いられる
  • シンプルなのは強みである一方で、基本的にはモノリシックなので、弾力性などは低い

12章「マイクロカーネルアーキテクチャ

13章「サービスベースアーキテクチャ

バランス型、という感じで、書籍の中でも割と推されているような記述ぶりに感じた。

14章「イベント駆動アーキテクチャ

  • 非同期的にイベントを送受信して処理するアーキテクチャ
  • リクエストベースモデルとイベントベースモデルという分類がある
  • ブローカートポロジーは、イベントの処理が終わったのち、受信する人がいるかどうかを気にせずイベントを発生するもの。拡張性に優れるが、ワークフローを管理する役割を持つ機能が不在
  • メディエータートポロジーは、イベントの受信を請け負って、ワークフロー全体の動向を管理するが、その分拡張性はブローカーほどではない
  • 他のアーキテクチャとハイブリッド形式で採用されることも多い

15章「スペースベースアーキテクチャ

  • キャッシュを使った(DBへの接続回数を減らす)アーキテクチャ
    • 複数の「処理ユニット」間で、データのキャッシュをレプリケーションして同期的に持つ(ただし、レプリケーションではなく、一元的にキャッシュを管理して各処理ユニットがリモートアクセスする分散キャッシュを利用することもある)
    • キャッシュを更新してからDBを非同期で更新する
  • それゆえ、更新のタイミングによってはキャッシュのデータに不整合が生じることもある
  • クラウドとオンプレミス環境を跨いでデプロイする(例: DBと読み書きをするデータリーダー/ライターだけオンプレにする)こともできる
  • これらの特徴から、コンサートチケットのサイトなどに適している(DBへのアクセスが少なく、弾力性が高い)
  • 一方で、非常に複雑なので、テストが難しく、コストも高くなる

16章「オーケストレーション駆動サービス指向アーキテクチャ

  • 過去に分散アーキテクチャとして考案されたものの、あまりうまくいかなかったもの
  • 再利用することに対するトレードオフは結合が高まること

この章の話は、全体的にあまりピンと来なかった

17章「マイクロサービスアーキテクチャ

  • 高度な分離をし、「境界づけられたコンテキスト」の論理的概念を物理的にモデル化することを目的としたアーキテクチャ
  • 「マイクロ」という名前は最小のという意味より、単なるラベル名と思うべき。トランザクションがサービス間を跨ぐことがないようなものであることは必要。
  • データベース含めて高度に分離されるので、各サービスで言語環境を変えるなどの組織計画も可能になる
  • サービス間で連携が必要な場合は、イベント駆動アーキテクチャでいう「ブローカー」や「メディエーター」のパターンを採用する
    • それぞれのメリット・デメリットはブローカー/メディエーターと同じ
  • どうしてもサービスを跨いでトランザクションを構築したい場合は、サーガという「メディエーター」の役割をするサービスを別途構築してそのサービスに管理させるという方法があるものの、結合してしまうトレードオフがあるので、基本的にはサービスを跨いでトランザクションを構築するのは推奨されず、サービスの粒度の見直しを優先的に検討するべき
  • コストや複雑性は課題だが、現在のエンジニアリングプラクティスで重視されるデプロイやテストの容易性に強みを持つ

19章「アーキテクチャ決定」

20章「アーキテクチャ上のリスクを分析する」

  • リスクマトリックスを利用し、アーキテクチャ内のどの部分にどのようなリスクがどのくらいあるかを分析する
  • リスクアセスメントとしてドメイン領域などで区切ってレポートにまとめる
    • この際、リスクの方向(改善か、悪化か)も重要な情報となる。リスクの方向の伝え方は工夫が必要(矢印などだと誤解を生みやすい)
  • 開発者(テックリード)と一緒にリスクストリーミングを行い、
    • リスクの特定(これは個人作業)
    • リスクの合意(どのアーキテクチャにどのレベルのリスクがあるかの合意を取る)
    • リスクの軽減(最も重要。合意したリスクに対して、それを軽減する方法を検討する。これを丁寧に行うことで、コスト増とのトレードオフ調整などがしやすくなる側面もある)

21章「アーキテクチャの図解やプレゼンテーション」

  • アーキテクトは良いアーキテクチャを考えても、きちんと伝えなければ意味がない
  • 図解を作成する上では、時間をかけすぎると作成した資料に執着が生まれてしまい、検討の邪魔になるので、最初は付箋などアナログな方法を活用してイテレーティブにできると良い
  • 図解を示したり、プレゼンテーションを工夫しないといけない

具体的な方法がいくつか述べられていた。今は生成AIを活用するのもよさそうと思った。

22章「効果的なチームにする」

  • アーキテクトが作成する制約はキツすぎても(コントロールフリーク)、緩すぎても(アームチェアアーキテクト)いけない
    • 開発者からアーキテクトになりたてだと、コントロールフリークに陥りやすい
    • 逆に、開発から離れていると、アームチェアアーキテクトに陥りやすい
  • どのように管理するのが良いかは、チームやプロジェクトの性質によるので、都度分析が必要
  • プロジェクトメンバーが多すぎると、多元的無知(自分の意見が言えなくなる)、責任の分散(自分で何かしようと思わなくなる)など、不具合が発生しやすくなる
  • チェックリストを活用する。静的解析など自動化できるものは自動化する
  • 開発者が技術スタックを使用したいときには、「ビジネス上の根拠」も確認するようにする
    • 開発者は、これが達成できないのに使用をしたいということがある

マネージャーとアーキテクトの棲み分けもなかなか難しそうと思った。
また、これを見るとアーキテクトは設計して終わりではなくて、開発・運用する間も一緒にプロジェクトを走らないと意味ないんだなーという感想を持った。

23章「交渉とリーダーシップのスキル」

  • 交渉
    • アーキテクトが下すほぼ全ての決定には異議が唱えられるため、交渉の力が非常に重要
    • ステークホルダーとの普段の会話から重要なアーキテクチャ特性を見極め、交渉の要素にすると良い
    • 交渉の際には、費用は時間は非常に重要であるものの、交渉の材料としては最後の手段にするべき
    • ステークホルダーが譲れないと言っている条件が、実はシステム全体ではなくシステムの一部が対象であり、その結果コストを抑えられることもある
    • 他のアーキテクトと衝突した場合は、議論しすぎるのではなく、冷静に実証することに努める
    • 相手が聞くのをやめてしまわないよう、根拠や理由を最初に説明する
  • リーダシップ
    • アーキテクトという肩書きで仕事をするのではなく、手本を示して周りを動かす
    • 開発者が質問や問題を相談できる相手になることを心がける
    • 開発チームに溶け込む時間を確保するため、アーキテクトは招待されたMTGについては自分の必要性を確認しつつ、開発者がMTGに巻き込まれているのを見たら、自分が代わりに参加して開発者の時間を確保することを検討する

交渉の方は、本で読むだけでしんどい仕事だなー、、、と感じた。
バズワードを利用する」というのは一般的には反感を買いそうな言葉だなーと思ったが、交渉のテクニックとしては必要か、、という気持ち。
リーダシップの方は、アーキテクトに関わらずリーダの役割として仕事をする上で大切なことが書かれている気がした。
「肩書きで仕事しない」の箇所は、「役職で仕事をしないこと」と公務員をしていた頃に散々聞いてきた話を久々に聞いた感じがして懐かしい気持ちになった。

第24章「キャリアパスを開く」

  • 技術は目まぐるしく変化するので、常に学び続ける必要がある
  • 朝一、20分の時間を使って技術的な幅を広げるための時間を作る(「未知の未知」を「既知の未知」にする)
  • テクノロジーレーダーを活用して、自分が次に何を追求するかのビジョンを描く
  • ソーシャルメディアを使用して、「弱いつながり」(新しい仕事のオファーや普段の経験の外側からのアドバイスが期待できる)を広げる

みんな分かっているけどなかなかできないよね、、という章。
ただ、20分を使って、「未知の未知」を「既知の未知」にする、というのはハードルが下がってよさそうな気がした。
あとSNSは、どうしても自分には向いていないと思ってしまうので、どうしたもんかな、、、という感想でした。

『リファクタリング 第2版』読書メモ

積読していた『リファクタリング 第2版』を読みました。

www.ohmsha.co.jp

どんな本?

リファクタリングとは、リファクタリングの意義を前半で解説した上で、後半(6章〜)はリファクタリングのサンプルカタログのようになっています。

私は、1-5章は丁寧に読み、6章以降は、具体的な手順はそこそこに、どんなケースでどんなリファクタリングをするべきなのか、という点を確認していく読み方をしました。

感想

自分が考えていたリファクタリングは、本書で解説されているリファクタリングより少し狭かったなーと感じました。

というのも、自分の考えていたリファクタリングは、

  • 従来よりもコード数が少なくなる(よりスッキリ書く)
  • 適切なクラスに責務を負わせる
  • 実装効率が改善する

などで、「可読性」についてはちょっと意識が少なかったかも、、という感想を持ちました。

以下、「印象に残った箇所」でも紹介していますが、 例えば「せっかく一度読んだコードの意味を覚えておくために関数化する」「可読性を採った結果、多少非効率に見える実装になっても、パフォーマンス上は実は無視できるレベルであることも多い」というのは、あまり考えていなかったポイントでした。

本書を読み進めている最中から、早速実務の方でも意識的に取り組んでみましたが、「可読性」を重視したリファクタリングは、結果スッキリ見えるし、メンテナンス性も上がるよなーという印象です。

印象に残った箇所

第1章

  • P7

理解したコードを埋め込んでおく方法とは、意味のあるコードの塊を関数に

処理をまとめて関数にすることの理由を、再利用とか見やすくするとかぼんやり理解していたが、「せっかく理解したコードの内容を忘れないようにする」ということをしていたんだなと気づきがあった。

  • P14

ローカル変数を削除することによる大きな利点は、扱うべきローカルスコープが減ることにより、メソッドの抽出がずっと楽になること

この箇所では、ローカル変数を削除する代わりにメソッドを何度も呼ぶことになるコードが紹介されていた。 自分だったら、何度も計算するのが嫌でローカル変数にすぐ入れてしまうなーと思ったので、パフォーマンス上問題ない限りは、リファクタリングする上ではローカル変数は減らすべきというのは頭に入れていきたい。 筆者は、別の箇所で、パフォーマンス改善もリファクタリングしてからの方がしやすい、とも言っている。一度パフォーマンスのことは気にせず、整理してからパフォーマンスに向き合うというのもアリなんだなーという気づきを得た。

  • P27あたり

中間データ構造を設けて、計算を担当する部分と表示を担当する部分を切り離すというもの。 中間データ構造を設けることで、関連する関数同士を切り離せるようにするという発想は色々な場面で使えそう。

  • P33

「簡潔さは知恵の要」と言われますが、明確さは進化するソフトウェアの要

コード量が長くなるのは良くないと思いがちだが、それによって明確さが増すのであればむしろ良いコードになるということを意識したい。

第2章

  • P46

ここのリファクタリングは非常に小さいステップ、またはそれらの組み合わせてでできています。その結果、リファクタリングではコードが壊れた状態になっている期間は非常に短く、たとえ未完成であっても、いつでも中断が可能です。

大規模なリファクタリング計画があったとしても、小さいステップで手順を踏み、その都度テストが通るように行なうリファクタリングを積み重ねるというプロセスを取るべきというもの。 実際に私も、リファクタリングするぞ、となった時、ついついいきなり大きく変更を加えてしまうことがあるが、これではリファクタリングになっていないということを意識しないといけないと感じた。

リファクタリングの途中で気がついたバグについては、リファクタリングも残っているべき

リファクタリング中にバグに気づくのはあるあるで、つい直してしまうが、勇気を持って見逃す(メモはしておく)というのも時には必要。 今までは、気付いたからには直さないといけないのでは?という気持ちで、リファクタリングを中断してバグ修正を先に行うなどしていたが、この書籍を以て、バグの程度によってはバグを保ったままリファクタリングを続けるという選択肢も持っておきたい。

  • P57

リファクタリングは、コードベースがどれだけ美しいかではなく、純粋に経済的な基準で測られるもの

あくまでリファクタリングは機能追加等をしやすくするためのもので、3行で書いていたコードをトリッキーなことをして1行にした!(ただし、読みづらくなっている)というような類ではないよ、という話。 これは結構勘違いしやすいところがある気がする。

  • P74, P75

良い名前が思いつかないということは、設計がまだ固まっていないことの兆候でもあります


コメントが必要だと感じたとき、代わりにわかりやすい名前を付けるようにする

今まで何度か聞いた言葉だが、改めて意識したい。

  • P236「ループの分離」

1つのループで複数のことをやらず、ループを分離しよう、というもの。 書籍の例に漏れず無駄に2回ループ回すのを避けてしまうが、パフォーマンス上問題ないケースも確かに多くありそう。

自分はポリモーフィズムを使うというリファクタリングをする勇気が持てないことが多いので、ここにあるガイドブックをもとに少しずつ挑戦してみようかな、、という気持ちになった。

  • p 332, p335 「問い合わせによるパラメータの置き換え」「パラメータによる問い合わせの置き換え」

どちらを取るかという1つの基準は「参照透過性」というのはあまり考えたことがなかったので、参考になった。

CSVを使った一括インポート機能を作成する際に確認するポイント

先日、CSVファイルを使ったデータの一括インポート機能を作成する機会がありました。 一連の実装を振り返ると、気にすることや決めるべきことが色々あるなーと思ったので、メモしておきます。

異常系への対応

✅ ファイルの形式が間違っているケースの対応をする

  • HTMLのaccept属性で制限するのとは別に、csvファイル以外がアップロードされた場合のサーバーサイドでの対策が必要です
  • 例えばrubycsvライブラリでは、CSV::MalformedCSVErrorが発生するので、それをrescueする処理を書いておくのが1つの方法です
begin 
  CSV.parse(...)
rescue CSV::MalformedCSVError
  flash.now[:alert] = 'CSVファイルをアップロードしてください'
end

✅ インポートできるデータの上限数を決めておく

  • 無制限にしてしまうと、サーバーの圧迫などを招く可能性があります
  • したがって、例えば以下の要素を検討し、適切な上限を設定する必要があります
    • ユーザーが不便に感じない上限数
    • パフォーマンス上問題ない上限数
    • プレビュー画面で表示した際にユーザーが見やすい上限数

✅ インポートされたデータが0件のケースの対応をする

  • 意外と忘れがちなのが、ユーザーがテンプレートをそのままアップロードしてしまった場合など、登録するべきデータが0件の場合です。
  • 何も対策していないと、バリデーションなどを通過してしまい、保存処理時に500エラーが起きる、といったこともあり得るので、0件の場合はきちんとエラーを返して保存処理に進ませない対策が必要です

ユーザービリティ関係の対応

✅ 何を記入したらいいかわからないようになっていないか

  • 電話やメールアドレスなどは明らかですが、DBの制約上、複数の選択肢から選んで欲しい場合(例えば「職種」など)などもあります
  • 何も説明がないと、自由記述とユーザーは思ってしまうので、何かしらの対策が必要です
  • 選択肢が少ない場合は、行のところに「職種(エンジニア or その他 で入力)」など書いておけばいいですが、選択肢が多い場合は非常に悩ましいです

✅ (記入例行がある場合)記入例行を削除するユーザーに対応できているか

  • 「何を記入したらいいかわからないようになっていないか」とも関連しますが、例えばテンプレートに記入例行を設けておく場合などは取り扱いに注意が必要です
  • 機械的に「上から1行は記入例だから無視」としてしまうと、記入例に上書き・削除して記載するユーザがいた時に、最悪保存できたつもりが保存できていない、という状況に陥り、不具合報告につながる可能性があります
  • 記入例行が記入例と違えば登録処理の対象に入れる、という仕様もありますが、仕様が複雑になったり、ユーザーのわかりづらさにつながるので、個人的には、可能な限り記入例行を作らずにテンプレートは作成できると良いのかなーと思っています

✅ バックグラウンド処理を使わずに実装できないか

  • 一括インポートは当然処理時間が長くなりがちのため、バックグラウンド処理が1つの選択肢となります
  • ただし、バックグラウンド処理をすると、考えるとが増えるので、できればbulk insertなどを有効活用しながら、同期処理で行うのが良いと思います
    • この点については、morihirokさんのKaigi on Rails 2025での発表が非常に参考になります

speakerdeck.com

【Rails】transactionのrequires_newの挙動について整理する

先日、Railsのtransactionのrequires_newオプションを知りました。

techracho.bpsinc.jp

tech.smarthr.jp

記事などを見れば理解できるのですが、(非推奨の):joinableと混同して「どっちだっけ?」となるので自分なりに表にまとめて整理しました。

まとめ

No. requires_new 例外の種類 ロールバックする処理
1 false false AR:Transaction 子の処理もロールバックしない
2 true false AR:Transaction 子の処理もロールバックしない
3 false true AR:Transaction 子の処理のみロールバックする
4 true true AR:Transaction 子の処理のみロールバックする
5 false false AR:Transaction以外 親の処理までロールバックする
requires_newは無関係

<補足>

  • 検証バージョン: Rails.8.0
  • requires_newfalseがデフォルトです
  • AR::RollbackActiveRecord::Rollbackの略です
  • 「親の処理」「子の処理」は以下のイメージです
ActiveRecord::Base.transaction do
  Car.create! # ←「親の処理」と呼んでいます
  ActiveRecord::Base.transaction do
    Bike.create! # ←「子の処理」と呼んでいます
    # ここで例外が起きるイメージ
  end
end

検証用コード

上記の表No.1~No.5について、検証用のRSpecの結果を書いておきます。

No.1

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(1)
end

# ↓結果
# 1 example, 0 failures

No.2

it do
  expect do
    ActiveRecord::Base.transaction(requires_new: true) do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(1)
end

# ↓結果
# 1 example, 0 failures

No.3

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction(requires_new: true) do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

No.4

it do
  expect do
    ActiveRecord::Base.transaction(requires_new: true) do
      Car.create!
      ActiveRecord::Base.transaction(requires_new: true) do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

No.5

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        nil.foo # NoMethodError
      end
    end
  end.to raise_error(NoMethodError)
    .and change(Car, :count).by(0)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

【Rails】debug_exception_log_levelが効かない?と思ったらbetter_errorsが制御していた

前回に引き続き、better_errors gemによって想定とは違う挙動をしていた体験をしたので、記事にしました。

↓参考: 前回の記事

blog.m-ito27.com

↓better_errors

github.com

背景: Rails7.1から例外のデフォルトログレベルがWARNになった

先日、とあるWebアプリのRailsアップグレード(Rails7.1→Rails8.0)をしていました。
config.load_defaultsが古かったので、Railsに合わせて引き上げる作業をしていたのですが、Rails7.1からconfig.action_dispatch.debug_exception_log_levelのデフォルトの値が従来の:fatalから:warnに変更されました。

railsguides.jp

この設定が変更されたPRを確認すると、「Ruby LoggerのドキュメントではFATALは『プログラムのクラッシュを引き起こす、処理不能なエラー』と定義されており、DebugExceptionがエラーを処理している現状と合わない。」という旨の説明がされています。

github.com

設定値変えてもログに変化がない?

そこで、この設定の値を :warnにして、ローカル環境で例外を発生させてみたのですが、ログには FATALとして表示されました。 ちなみに、ログレベルはseverityを使って表示できます。

config/environments/development.rb(簡略化しています。)

config.action_dispatch.debug_exception_log_level = :warn
config.log_formatter = proc do |severity, time, progname, msg|
  "[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- #{progname}: #{msg}\n"
end

出力されるログのサンプル

[2025-09-14 23:17:44] FATAL -- : 
RuntimeError - 例外が発生:
  app/controllers/...

あれ?ここがWARNになるってことではないの、、?

better_errorsがログレベルを制御していた

アプリ内のコードを確認しても、特別ログレベルをカスタムしている場所はなかったので、Gemfileを確認すると、better_errorsがあり、怪しそうに思ったので試しにbetter_errorsコメントアウトしてみました。

すると、、、

[2025-09-14 23:45:39] WARN -- :   
RuntimeError (例外が発生):

WARNになりました!

具体的には、以下の箇所で制御されていました。

better_errors/lib/better_errors/middleware.rb

def log_exception
  return unless BetterErrors.logger

  message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
  message += backtrace_frames.map { |frame| "  #{frame}\n" }.join

  BetterErrors.logger.fatal message # ←ここ
end

実際にここをwarnに変えたらログはWARNで表示されました。

さいごに

コントリビュートチャンス?と思いましたが、better_errorsは2年前を最後にコミットが追加されておらず、メンテが止まっているようです。
内容が軽微なこともあり、モンキーパッチを当てるなどはせず、「config.load_defaultsに合わせてデフォルト値は変わるが、ローカル環境のログ上はFATALのまま」、というのを許容することにしました。
better_errosの良い代替gemってあるのかなー、、とぼんやり思ったりしています。

【Rails/RSpec】better_errors gemをテスト環境で有効にしていたらハマった

better_errorsは、Railsのデフォルトエラーページよりもエラーを詳細に表示し、デバッグを助けてくれるgemです。

github.com

READMEにある通り、基本的にはdevelopment環境での使用が想定されています。
しかしながら、今回、test環境で有効にしていたことによりハマったことがあったので、記録として残しておきます。

何が起きたか

テストを書いていたら、以下の状況になりました。

「例外を起こすことのテストで、RSpec実際に例外が発生しているはずなのにraise_errorするとテストが失敗するぞ?

↓実際に書いたテストを簡略化したもの

it do
  expect { get root_path }.to raise_error RuntimeError # root_pathにアクセスすると例外が発生するようにしているので、それを確認するテスト
end

↓結果

Failure/Error: expect { get root_path }.to raise_error RuntimeError
       expected RuntimeError but nothing was raised

画面上でアクセスしても例外が発生するのに、テストすると「例外なんて発生していないよ」と言われ、結構混乱しました。

原因: better_errorsがテスト環境にインストールされていた

色々試行錯誤する中で、悪さをしているライブラリがないかとGemfileを見ると、あることに気づきました。

group :development, :test do
  gem 'better_errors'
end

あれ、better_errorsってテスト環境で有効にしなくても良いものでは?と思い、とりあえず消してみました。

↓修正後のGemfile

group :development do
  gem 'better_errors'
end

結果、、、

Finished in 0.02689 seconds (files took 1.18 seconds to load)
1 example, 0 failures

これかー!

better_errorsをtest環境で有効にしてしまっていたのが原因とわかりました。 では、具体的に、なぜbetter_errorsをテスト環境に入れていると例外が捕捉できないのでしょうか?

better_errorsは例外をrescueする

better_errorsのコードを読んだら、例外をrescueして、カスタマイズしたエラー画面を表示していました。 そして例外はそのまま伝播させず、500のレスポンスとして返すようにしていました。

better_errors/middleware.rb

def protected_app_call(env)
  @app.call env
rescue Exception => ex
  @error_page = @handler.new ex, env
  log_exception
  show_error_page(env, ex)
end

def show_error_page(env, exception=nil)
  # ...(略)
  [status_code, headers, [content]]
end

だからRSpec側でraise_error XXXしても、例外は発生していない、と言われたんですね。

さいごに

しばらく、better_errorsというところに当たりをつけられず解決まで少し時間を要しましたが、原因がわかってスッキリしました。
このリポジトリbetter_errorsを導入したのは私ではないのですが、おそらくsystem specの失敗時のスクリーンショットからデバッグをしやすくするために入れたのかなーと推測しています。

実はこれ以外にもbetter_errorsによって想定と違う動きをした、という例があるのでまた記事にして残しておきたいと思います。

『暗号技術入門』読書メモ

cr.hyuki.net

少し前に『暗号技術入門』(第1部、第2部)を読んだので読書メモを残しておきます。

第2章

シーザー暗号

  • 文字ずらしで暗号化する方法

  • シーザー暗号をRubyで書くと、、を考えてみた。

    • 自分で考えた方法(nextを使う)
class String
  def encrypt_text(shift_int)
    splited_str = chars
    shift_int.times do
      splited_str.each { _1.next! }
    end
    splited_str.join
  end
end

puts '123あいうえおABC'.encrypt_text(3)
#=> 456ぅぇぉかきDEF
  • chatGPT先生に聞いたところ、アルファベット限定だが、rotatetrを使う方法が返ってきた。なるほどー。
def shift_string(str, shift)
  alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  shifted_alphabet = alphabet.chars.rotate(shift).join
  str.tr(alphabet, shifted_alphabet)
end

puts shift_string("ABD", 3)
#=> "DEG"

単一換字暗号の解き方

  • 単一換字暗号とは、文字を不規則に1対1で置き換える方法(aはc, bはf, cはiなど)
  • 組み合わせはアルファベットだと26 * 25 * .. * 1の403291461126605635584000000通りあり、一見解析は困難に思える
  • しかし、頻度分析(アルファベットだとtheなどが多く、推測しやすい。また、頻度が少ないものもヒントになる。)を使うと、単純計算では組み早稲が膨大であっても解析される。
  • 感想
    • どこかでこの解法は見た記憶があるが、改めてなるほどーーと感じた。

秘密にするべきは暗号方式ではなく、鍵

  • 暗号化方式は秘密にしていても必ず解析されるため、秘密の暗号化方式を使うことで強化にはならない(むしろ脆弱になる)
  • したがって、秘密にするべきは鍵
  • 暗号化方式がわかっていても(いずれ解析できる)、鍵を秘密にすることで機密性を高めることができる。

第3章

XOR

  • 本筋とは関係ないが、本書で紹介されていたXORの説明が、わかりやすいかもと思ったのでメモ

    • 裏返すor裏返さないの行動を2回行った結果、オセロが最初の状態と比べて裏返されている(1)かと考えるとわかりやすい
      • 2回とも裏返す, 2回とも裏返さない → 元の面なので0
      • どちらか1回だけ裏返す → 裏返っているので1
  • A XOR B の結果を またXOR Bすると、Aに戻るというのはRubyで近しいコードが作れる

require 'set'

set = Set[1, 3, 5, 6, 7, 10]
key = Set[2, 5, 7, 10]

p set ^ key

p (set ^ key) ^ key # setと同じになる。

使い捨てパッド

  • 平文と同じ長さの完全にランダムなビット列(鍵)を用意し、XORをしたものを暗号文とするもの
  • 数学的に解読不可能であることが証明されているが、鍵の配送問題など使いづらい点が多い

AESの公募

  • NISTがAESを公募
  • プログラムが公開され、無料で使えることなどが条件
  • 応募者たちは、自分の提出したものを通すため、他の応募者の暗号方式の弱点を調べ上げることになった => 正しいコンペのかたちが実現された

第5章

RSAの由来

(本筋の話ではないが、あ、そうなの、と思ったのでメモ。)
技術的な名称の略ではなく、3人の開発者(Rivest-Shamir-Adleman)の頭文字。

RSAの暗号と複合

  • 暗号化の計算式は、暗号文 = 平文のE乗 mod N (EとNが公開鍵)
  • 複合化の計算式は、平文 = 暗号文のD乗 mod N (DとNがプライベート鍵。Nは共通)

暗号化の流れ

  • とある素数p, qをかけた数である、Nを求める
  • p-1とq-1の最小公倍数であるLを求める
  • 1より大きくLより小さい、Lとの最大公約数が1となるEを求める
  • 1より大きくLより小さい、E*D mod L = 1となるDを求める

なぜ安全?

  • 公開鍵情報に含まれるのはEだが、Eを作るために使用したp, q, Lを解読者は知らない
  • N = p * qのため、p, qが求められそう?だが、大きな数の素因数分解を高速にできる方法はまだ発見されていないので、安全と言える
    • 逆にこれが可能になったら解読可能

第6章

  • 疑似乱数発生器で生成した「セッション鍵」を対称暗号方式の鍵として、メッセージ(平文)を暗号化
  • 上記セッション鍵を公開鍵暗号方式で暗号化
  • これにより、公開鍵暗号方式の遅いというデメリットと対照暗号方式の鍵配送問題を解決している

第7章

  • 弱衝突耐性と強衝突耐性
    • 若干言葉と意味に自分的にはズレがあった
    • 弱衝突耐性
    • 強衝突耐性
      • 同じハッシュ値となる、異なる2つのメッセージを求めることが非常に困難であること
  • つまり、弱衝突耐性があれば、強衝突耐性も満たしていることになる

  • SHA-3

    • SHA-1の強衝突耐性が破られたことで新しく公募された
    • KECCAK(ケチャック)というアルゴリズムに決まった
      • そのうち、採用条件には「クリーンな構成で解析しやすいこと」というのがあった
        • 逆じゃないの?と思うかもしれないが、解析しやすい=弱点を見つけやすい→それでも弱点がないなら強いよね、ということみたい
  • 感想

    • ここまで読んでいて、Railsアプリでよく使われるdevise gemは bcryptだけど、SHA-3とかとは違うのだろうか?と思ったので少し調べた
      • bcryptはパスワードのために作られた方式で、ソルトを使った方式がデフォルトだったり、意図的に処理速度を遅くしてパスワード一覧攻撃に強くしていていることが特徴

第8章

  • 上記まででは、メッセージが正しい送信者から送られて来たものか?(認証)ができていない
  • そこで、メッセージ認証コード(MAC)という技術がある
    • 送るメッセージに対して共有鍵を使ってハッシュ化する(MAC値)
    • 送信時、メッセージと共にMAC値を送信する
    • 受信者側で、同じく持っている共有鍵を使ってMAC値を作成し、一致すれば鍵を共有している相手から来たメッセージと判断できる

第9章

  • そのメッセージを送信したのが誰か、を証明できるようにする技術が、デジタル署名
  • 公開鍵暗号方式を逆に使うことで実現している
    • 署名作成側がメッセージをハッシュ化したものを秘密鍵で暗号化
    • 検証側で公開鍵で復号し、検証

第10章

  • デジタル署名の発行元を信頼するための仕組みが、認証局
  • 認証局は、(本人確認ができている)署名作成者の公開鍵を受け取り、認証局秘密鍵でデジタル署名したものを、作成者の公開鍵に付与した上で、受信者に送付する
  • 受信者は、認証局の公開鍵でデジタル署名を検証し、検証できたら受け取った作成者の公開鍵が正当なものであることを確認できる

おわりに

偶然なことに、この本を読んでから実務の方でもAPIの開発で暗号化や署名を付与する実装が必要になり、この本で学んだ内容が役に立ちました。 第3部は読むのに少しパワーが要りそう?で(他にやりたいこともあり)一度置いてしまいましたが、またそのうち読みたいと思います。