(AI 時代にジュニア開発者が直面する変化)= # AI 時代にジュニア開発者が直面する変化 本書の最終章に入りました。 Vol.1「本書の対象読者とゴール」から{numref}`例外はどこで拾われるのか`({ref}`例外はどこで拾われるのか`)までの道のりを振り返ると、リクエストがブラウザを離れてからレスポンスとして返るまでの長い旅路を、Django と FastAPI の両面から追いかけてきました。 これまで学んできた内容を改めて整理すると、次のような知識体系になります。 - **WSGI と ASGI の違い**: 同期・非同期のリクエスト処理モデルの根本的な差異 - **ミドルウェアの連鎖**: リクエスト・レスポンスを横断的に処理する仕組み - **ORM が SQL を組み立てる仕組み**: Python のコードがどのようにデータベースクエリに変換されるか - **並行処理のモデル**: スレッド・プロセス・コルーチンの使い分け - **本番環境のデプロイ構成**: 開発環境と本番環境の根本的な違い - **セキュリティの多層防御**: 攻撃を複数の層で防ぐ設計思想 これらはいずれも「Web アプリケーションの内部で何が起きているか」を理解するための知識です。 最終章では、この知識が LLM がコードを書いてくれる時代にどのような意味を持つのかを考えます。 以降では、AI 時代にジュニア開発者が直面している3つの変化を見ていきます。 ```{mermaid} flowchart LR L[LLM] -->|コード生成を加速| SP[速く作れる] SP -->|内部理解が置き去りに| IG[障害対応で差が出る] IG -->|人間の価値| MO[保守・障害対応
セキュリティレビュー] ``` ## 早く作れる LLM の登場により、コードを書く速度は劇的に上がりました。 「Django で商品一覧と詳細ページを持つ EC サイトの雛形を作って」と指示すれば、モデル定義、ビュー、URL 設定、テンプレートまで含む動作可能なコードが数十秒で生成されます。 FastAPI であれば、Pydantic モデルと CRUD エンドポイント、SQLAlchemy との接続コード、さらには Dockerfile まで一気に出力されることも珍しくありません。 この速度は本物です。 かつてジュニア開発者が数日かけて公式ドキュメントとチュートリアルを行き来しながら構築していた雛形が、プロンプトひとつで手に入ります。 LLM が特に威力を発揮する場面は、次のようなケースです。 - **プロトタイピングの初期段階**: アイデアを素早く形にする - **社内ツールの素早い構築**: 品質より速度が優先される場面 - **概念実証(PoC)の作成**: 技術的な実現可能性を確かめる これらの場面において、LLM は圧倒的な生産性を提供します。 この恩恵を否定する必要はありません。 ```{important} 「早く作れる」ことと「正しく作れている」ことの間には、目に見えにくいが決定的な溝があります。 LLM が生成したコードは質問に対する「最も確からしい回答」であり、プロジェクト固有の制約、チームのコーディング規約、本番環境のインフラ構成、そしてセキュリティ要件を十分に考慮したものとは限りません。 ``` たとえば 14-4 で触れたように、生成されたコードに入力値のバリデーションが抜けていることは日常的に起こります。 また 12-6 で学んだワーカー設計の知識がなければ、`runserver` でデプロイしてしまうコードがそのまま本番に載ることもあります。 ## でも内部理解が置き去りになりやすい 速度の向上には副作用があります。 LLM が「動くコード」を即座に提供してくれるため、なぜそのコードが動くのかを理解する動機が薄れてしまうのです。 たとえば、Django の `get_object_or_404` を使ったビューが LLM によって生成されたとします。 コードとしては正しく動作しますが、ジュニア開発者がその内部を知らなければ、次の事実を理解しないまま先に進むことになります。 - `get_object_or_404` は `ObjectDoesNotExist` 例外を `Http404` に変換している - その例外は 14-1 で解説したフレームワークの例外ハンドラチェーンを通じて 404 レスポンスになる - `DEBUG = False` では `handler404` に設定されたビューが呼ばれる 平時はそれで問題ありませんが、障害時には話が変わります。 12-8 で整理したトラブルシューティングの観点を思い出してください。 CPU バウンドか I/O バウンドかの切り分け、イベントループのブロック検出、DB コネクション数との整合性の確認――これらは「内部で何が起きているか」の理解なしには実行できません。 LLM に「本番で 502 が出ています。原因を教えてください」と聞いても、nginx のログ、Gunicorn のワーカー状態、アプリケーションのエラーログ、DB のコネクション状況を総合的に読み解くのは人間の仕事です。 この読み解きには、13-4 で学んだタイムアウトの連鎖、13-2 のリバースプロキシの役割、12-6 のワーカー設計の知識が前提として必要です。 ```{note} LLM は「知識の入口」を大幅に広げてくれる道具ですが、「知識の深さ」を自動的に与えてくれるものではありません。 生成されたコードをそのまま使うだけの開発者と、生成されたコードの内部構造を理解して必要に応じて修正できる開発者の間には、平時には見えにくいながらも障害時に決定的な差が現れます。 ``` ## 保守と障害対応の価値が上がる AI がコードを書く時代に、人間の開発者の価値はどこに移動するのでしょうか。 一つの明確な答えは、**保守と障害対応**です。 新規コードの生成は LLM が得意とする領域です。 しかし次のような作業は、LLM だけでは完結しません。 - 既存のコードベースを読み解き、数年前に別のチームが書いたミドルウェアの挙動を追跡する - 本番環境で発生している間欠的な障害の原因を、nginx のログ・Gunicorn のワーカータイムアウト・PostgreSQL のコネクション上限の三者の関係から特定する - コードだけでなくインフラ構成、ネットワークの状態、時系列のログデータ、ビジネス上の影響範囲を同時に把握する 本書が{numref}`なぜ Web 開発で並行処理が重要なのか`({ref}`なぜ Web 開発で並行処理が重要なのか`)で並行処理を、{numref}`開発環境と本番環境は何が違うのか`({ref}`開発環境と本番環境は何が違うのか`)でデプロイ構成を、{numref}`例外はどこで拾われるのか`({ref}`例外はどこで拾われるのか`)でエラーハンドリングとセキュリティを詳しく扱ったのは、まさにこの保守・障害対応フェーズで必要になる知識だからです。 LLM が生成した CRUD エンドポイントは動きますが、そのエンドポイントが本番で毎秒数百リクエストを受けたとき、Gunicorn のワーカーが足りなくなり、DB コネクションプールが枯渇し、nginx が 502 を返し始める――この連鎖を理解して対処できるのは、内部構造を知っている開発者だけです。 さらに、LLM 時代のセキュリティレビューの重要性も増しています。 14-5 で挙げた XSS、CSRF、SQL インジェクションなどの脅威に対して、生成されたコードが適切に対策されているかを検証するのは人間の責任です。 ```{warning} LLM は「一般的にはこう書く」というパターンを出力しますが、プロジェクト固有のセキュリティ要件(たとえば「この API は社内からのみアクセス可能にする」「ユーザー入力の HTML を保存する場合は nh3 でサニタイズする」)を LLM が自発的に適用してくれることは期待できません。 ``` 本書を通じて得た「フレームワークの向こう側」への理解は、LLM と共存する時代においてこそ価値を持ちます。 速く作ることは LLM に助けてもらいながら、正しく動くことの検証と、壊れたときの復旧は自分の知識で行う。 この役割分担が、AI 時代のジュニア開発者がシニアへと成長するための道筋です。 次節では、LLM が生成するコードが「隠しがち」な領域を具体的に取り上げます。 (AI がよく隠してしまうもの)= ## AI がよく隠してしまうもの 前節では、LLM がコードの生成速度を劇的に向上させる一方で、内部理解が置き去りになりやすいという構造的な課題を指摘しました。 LLM にコード生成を依頼すると、出力されたコードは「動く」ことが多いのですが、その背後にある重要な設計判断や前提条件が暗黙のまま省略されるケースが繰り返し観察されます。 以降では本書の内容と重ね合わせながら、LLM が特に「隠しがち」な5つの領域を取り上げます。 | 隠されやすい領域 | 具体的なリスク | |---|---| | request/response の境界 | ミドルウェアの存在が見えない | | sync / async の違い | ブロッキング呼び出しで性能劣化 | | middleware の順序 | 依存関係のあるミドルウェアが誤動作 | | 本番サーバ構成 | 開発用コマンドが本番に流用される | | セキュリティ設定 | 本番向け設定が丸ごと省略される | ```{mermaid} flowchart TD H[LLM が隠しがちな領域] --> R[request/response の境界
ミドルウェアの存在] H --> SA[sync / async の違い
ブロッキング呼び出しのリスク] H --> MO[middleware の順序
依存関係と実行順] H --> PS[本番サーバ構成
ワーカー設計・graceful restart] H --> SC[セキュリティ設定
CSRF / CORS / SSRF 対策] ``` ### request/response の境界 LLM に「ユーザー情報を取得する API を作って」と依頼すると、ビュー関数やエンドポイントの内部ロジックが出力されます。 しかし、そのコードがどの時点でリクエストオブジェクトを受け取り、どの時点でレスポンスオブジェクトを返しているかの境界は、しばしば説明されません。 本書の前半で追いかけたとおり、Django では次のような流れでリクエストが処理されます。 1. `WSGIHandler.__call__()` でリクエストを `HttpRequest` オブジェクトに変換 2. ミドルウェアチェーンを上から順に通過 3. URL リゾルバがビュー関数を特定 4. ビューが `HttpResponse` を返す 5. ミドルウェアを逆順に通過してクライアントへ届く FastAPI では ASGI サーバが `scope`, `receive`, `send` の3つ組をアプリケーションに渡し、Starlette のルーティング層がエンドポイント関数を呼び出し、Pydantic がレスポンスをシリアライズして JSON バイト列に変換します。 LLM が生成するコードはこの旅路の「ビュー関数の中身」だけを切り出したものです。 たとえば Django のビューで `request.user` にアクセスしている場合、その `user` 属性は `AuthenticationMiddleware` が事前にセットしたものですが、LLM はミドルウェアの存在にほとんど言及しません。 FastAPI で `Depends()` を使った依存性注入のコードが出力されたとき、その依存関数がリクエストごとに呼ばれるのか、アプリケーション起動時に一度だけ呼ばれるのかを LLM が明示することも稀です。 この境界の理解が欠落すると、「ビューの外側で何が起きているか分からない」ままコードを積み重ねることになります。 リクエストの前処理でセッションが復元されていること、レスポンスの後処理で `Content-Length` や `Content-Type` が調整されていること、例外がビューの外側で捕捉されて適切なステータスコードに変換されていること――14-1 で解説した例外の多層構造は、この境界を意識しなければ理解できません。 ### sync / async の違い LLM に FastAPI のエンドポイントを依頼すると、`async def` で定義されたコードが返されることが多くあります。 一方 Django のビューを依頼すると通常の `def` で返されます。 ここまでは自然ですが、問題はこの2つの違いが何を意味するかを LLM がほとんど説明しないことです。 12-7 で「async = 常に高速」が誤解であることを学びました。 `async def` のエンドポイント内でブロッキング I/O(`time.sleep()` や同期的な DB ドライバの呼び出し)を行うと、イベントループ全体が停止し、そのワーカーが処理する全てのリクエストが巻き添えになります。 ```{caution} LLM が生成した FastAPI のコードに `import requests` が含まれていたとき、それが同期的な HTTP クライアントであり `async def` の中で使うとイベントループをブロックするという事実を、LLM は自発的に警告してくれないことがあります。 ``` ```python # LLM が生成しがちなコード(問題あり) @app.get("/external") async def fetch_external(): response = requests.get("https://api.example.com/data") # ブロッキング return response.json() # 正しい選択肢 A: async 対応ライブラリを使う @app.get("/external") async def fetch_external(): async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") return response.json() # 正しい選択肢 B: sync 関数にしてスレッドプールに任せる @app.get("/external") def fetch_external(): response = requests.get("https://api.example.com/data") return response.json() ``` Django でも ASGI デプロイ時に `async def` ビューと従来の `def` ビューが混在する構成では、同期ビューはスレッドプールで実行されるというランタイムの挙動を理解していなければ、性能特性を正しく予測できません。 LLM は「このビューを async にしますか?」と聞くことはあっても、「あなたのプロジェクトの DB ドライバは同期的なので async にしても恩恵はなく、むしろスレッドプール経由のオーバーヘッドが加わります」という文脈依存のアドバイスを自発的にはくれません。 ```{tip} `async def` と `def` の選択は「どちらが速いか」ではなく「使用するライブラリが非同期対応かどうか」で決めましょう。DB ドライバや外部 HTTP クライアントが同期的であれば、無理に `async def` にする必要はありません。 ``` ### middleware の順序 「Django プロジェクトに認証とロギングのミドルウェアを追加して」と LLM に頼むと、`settings.py` の `MIDDLEWARE` リストにエントリが追加されたコードが返されます。 しかし、追加された位置がなぜそこなのかが説明されることはほとんどありません。 本書で見てきたとおり、Django のミドルウェアはリクエスト時に上から下へ、レスポンス時に下から上へ処理されます。 それぞれのミドルウェアが上から配置されている理由は次のとおりです。 - `SecurityMiddleware` が最上位: 全リクエストに対して HTTPS リダイレクトやセキュリティヘッダの付与を最初に行うため - `SessionMiddleware` が `AuthenticationMiddleware` より上: セッションが復元された後でなければユーザーの認証状態を判定できないため - `CsrfViewMiddleware` が `AuthenticationMiddleware` より上: 認証の成否に関わらず CSRF 検証を先に行う必要があるため ```python # Django: ミドルウェアの順序には意味がある MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", # 1. セキュリティヘッダ・HTTPS リダイレクト "django.contrib.sessions.middleware.SessionMiddleware", # 2. セッション復元 "django.middleware.common.CommonMiddleware", # 3. URL 正規化 "django.middleware.csrf.CsrfViewMiddleware", # 4. CSRF 検証 "django.contrib.auth.middleware.AuthenticationMiddleware", # 5. ユーザー認証 "django.contrib.messages.middleware.MessageMiddleware", # 6. メッセージフレームワーク ] ``` LLM がカスタムミドルウェアを `AuthenticationMiddleware` の上に追加するか下に追加するかで、そのミドルウェアから `request.user` にアクセスできるかどうかが変わります。 ロギングミドルウェアを最上位に置けば全てのリクエストが記録されますが、`SecurityMiddleware` のリダイレクトで処理されたリクエストは本来のビューまで到達しないため、ロギングの位置によって記録内容が変わります。 FastAPI / Starlette でもミドルウェアの登録順序は重要です。 14-8 で触れたとおり、CORS ミドルウェアは他のミドルウェアより外側に配置しなければ、preflight リクエストが認証ミドルウェアに阻まれる問題が発生します。 ```{note} FastAPI では `app.add_middleware()` の呼び出し順序が**逆順**に適用されます。最後に追加したミドルウェアが最も外側(最初に実行される)で動作します。LLM はこのコードを生成してくれますが、この重要な仕様を説明してくれることは稀です。 ``` ### 本番サーバ構成 LLM に「FastAPI アプリをデプロイして」と依頼すると、`uvicorn main:app --host 0.0.0.0 --port 8000` というコマンドが返されることが多くあります。 14-7 で詳しく解説したとおり、これは開発サーバの起動コマンドであり本番構成ではありません。 しかし LLM の出力だけを見ると、これが本番に不適切であるという情報が添えられないことがあります。 本書で2章分を費やして学んだ本番構成の知識には、次のような要素が含まれます。 - Gunicorn のアービタープロセスによるワーカー管理 - `--workers` と `--threads` の適切な設定 - nginx によるリバースプロキシと TLS 終端 - 静的ファイル配信と graceful restart のシグナル制御 - Kubernetes でのヘルスチェックとローリングアップデート これらは LLM が「デプロイコマンド」として一行を出力する裏側に隠れている膨大な設計判断です。 ```{danger} `Dockerfile` を生成させると `CMD ["uvicorn", "main:app"]` で終わるコンテナが出力されることがあります。コンテナは起動しますが、13-7 で学んだ `startupProbe` や `readinessProbe` の設定、13-4 のタイムアウト整合性、13-5 の `terminationGracePeriodSeconds` と `--graceful-timeout` の関係は含まれていません。本番環境での安定運用に必要な設定はその外側にあります。 ``` ### セキュリティ設定 これは LLM が最も深刻に「隠す」領域です。 LLM にフォーム付きの Django ビューを生成させると、`{% csrf_token %}` が含まれていることは多いですが、`settings.py` における次のような本番向けセキュリティ設定はほぼ出力されません。 - `CSRF_COOKIE_SECURE` - `SESSION_COOKIE_SECURE` - `SECURE_SSL_REDIRECT` - `SECURE_HSTS_SECONDS` FastAPI の CORS 設定を依頼すると `allow_origins=["*"]` が返されることもあります。 14-8 で解説したとおり、これは `allow_credentials=True` との組み合わせで明確なセキュリティリスクになります。 14-5 で列挙した脅威のうち、SSRF とオープンリダイレクトは特に LLM が見落としやすい領域です。 「ユーザーが指定した URL の内容を取得するエンドポイント」を生成させると、14-5 で示した内部ネットワークへのアクセスを拒否するバリデーションが含まれないコードが出力されることがあります。 「ログイン後に元のページにリダイレクトする機能」を依頼すると、`next` パラメータを検証せずに `redirect()` に渡すコードが生成されることがあります。 ```python # LLM が生成しがちなコード(オープンリダイレクトの脆弱性) @app.get("/login") def login_view(request): # ... 認証処理 ... next_url = request.GET.get("next", "/") return redirect(next_url) # 14-5 で学んだ安全なコード from django.utils.http import url_has_allowed_host_and_scheme @app.get("/login") def login_view(request): # ... 認証処理 ... next_url = request.GET.get("next", "/") if not url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}): next_url = "/" return redirect(next_url) ``` LLM は「一般的なコードパターン」を出力することには長けていますが、「このプロジェクト固有のセキュリティ要件を満たしているか」を判断する責任は開発者にあります。 本書の{numref}`例外はどこで拾われるのか`({ref}`例外はどこで拾われるのか`)全体が、まさにその判断力を養うために書かれました。 ```{admonition} LLM 生成コードのレビューチェックポイント :class: tip 以上の5つの領域は、LLM が生成するコードをレビューする際の観点としても機能します。 生成されたコードを受け取ったとき、次の問いを順に確認してみましょう。 1. リクエスト/レスポンスの境界で何が起きているか 2. sync/async の選択は適切か 3. ミドルウェアの順序は正しいか 4. 本番構成として十分か 5. セキュリティ設定は漏れていないか この問いかけを習慣化することで、LLM の出力を「動くコード」から「正しく動くコード」へと引き上げることができます。 ``` 次節では、この問いかけを日常の開発フローに組み込む実践方法を見ていきます。 (「動くコード」をそのまま信じないための観点)= ## 「動くコード」をそのまま信じないための観点 前節では LLM が隠しがちな5つの領域を特定しました。 LLM が生成したコード、あるいは自分自身が書いたコードを目の前にしたとき、「ローカルで動いた」という事実だけで安心せず、4つの問いを投げかける習慣を提案します。 この4つの問いは本書全体の知識を凝縮したものであり、どれも「はい/いいえ」で即答できない問いです。 だからこそ、立ち止まって考える価値があります。 ```{mermaid} flowchart TD C[LLM 生成コードを受け取る] --> Q1{どの層のコードか
ビュー / ミドルウェア / サーバ設定} Q1 --> Q2{どの責務を担っているか
単一責任になっているか} Q2 --> Q3{例外時にどうなるか
異常系は考慮されているか} Q3 --> Q4{本番で動くか
マルチプロセス / HTTPS / タイムアウト} Q4 --> OK[正しく動くコード] ``` ### どの層のコードか 最初の問いは「このコードは、リクエストがたどる旅路のどの地点で実行されるか」です。 本書の前半で追いかけたとおり、Web アプリケーションには明確な層構造があります。 クライアントから届いたリクエストは、次の順序で各層を通過し、レスポンスは逆順にクライアントへ戻ります。 1. リバースプロキシ(nginx) 2. WSGI/ASGI サーバ(Gunicorn / Uvicorn) 3. ミドルウェアチェーン 4. ルーティング 5. ビュー / エンドポイント 6. ORM / データベースドライバ LLM が生成するコードのほとんどはビュー層、つまり旅路の中間地点に位置するものです。 しかし同じ「リクエストの処理」でも、ミドルウェアに書くべきコードをビューに書いてしまうケースや、逆にビュー固有の処理をミドルウェアに入れてしまうケースが頻繁に発生します。 たとえば、全リクエストの処理時間を計測してログに残す機能を考えます。 LLM に依頼すると、各ビュー関数の冒頭と末尾に `time.time()` を挟むコードが生成されることがあります。 動きはしますが、これはミドルウェアで一箇所に集約すべき横断的関心事です。 ビューが20個あれば同じ計測コードが20箇所に散らばり、1つでも書き漏らせばそのエンドポイントだけ計測されません。 ```python # 層を誤った例: ビューに横断的関心事を埋め込む @app.get("/orders/{order_id}") async def get_order(order_id: int): start = time.time() order = await fetch_order(order_id) duration = time.time() - start logger.info(f"get_order took {duration:.3f}s") return order # 正しい層: ミドルウェアで全リクエストを計測する @app.middleware("http") async def timing_middleware(request: Request, call_next): start = time.time() response = await call_next(request) duration = time.time() - start logger.info(f"{request.method} {request.url.path} {duration:.3f}s") return response ``` 反対に、特定のエンドポイントでのみ必要な認可チェック(「この注文は自分のものか」)をミドルウェアに書こうとするのも層の選択を誤っています。 ミドルウェアは全リクエストに適用されるため、URL パターンによる分岐が増えて可読性が下がります。 Django ではビューデコレータや `get_object_or_404` と `PermissionDenied` の組み合わせ、FastAPI では `Depends()` を使った依存性注入がこの層に適した手段です。 ```{tip} コードを受け取ったら、まず「これはミドルウェアか、ビューか、ORM の設定か、サーバの構成か」を確認しましょう。 層が正しければ保守性が上がり、層を誤ると同じロジックが散在して将来の変更コストが跳ね上がります。 ``` ### どの責務を担っているか 2つ目の問いは「このコードは何をしていて、何をしていないか」です。 LLM が生成した1つのビュー関数を読むと、リクエストの受け取り、入力値のバリデーション、ビジネスロジックの実行、データベースへの書き込み、レスポンスの構築が一体化していることがよくあります。 100行に満たないコードであれば読めなくはありませんが、このコードには複数の責務が混在しています。 本書を通じて学んだフレームワークの設計思想は、責務の分離を一貫して推奨しています。 | フレームワーク | 責務の分担例 | |---|---| | Django | フォーム/シリアライザ → バリデーション、モデル → データの整合性、ビュー → リクエスト/レスポンスの橋渡し | | FastAPI | Pydantic モデル → バリデーション/シリアライズ、`Depends()` → DB セッション/認証情報の注入、エンドポイント → 薄いオーケストレーション層 | 責務が混在したコードの典型的な問題は、テストの困難さとして表面化します。 データベースに依存するビジネスロジックがビュー関数に直接書かれていると、そのロジックをテストするために毎回 HTTP リクエストをシミュレートする必要があります。 ロジックを独立した関数やサービスクラスに切り出しておけば、単体テストで直接呼び出せます。 ```python # 責務が混在した例 @app.post("/orders") async def create_order(data: OrderCreate, db: Session = Depends(get_db)): if data.quantity > 100: raise HTTPException(400, "一度に100個以上は注文できません。") product = db.query(Product).filter(Product.id == data.product_id).first() if not product: raise HTTPException(404, "商品が見つかりません。") if product.stock < data.quantity: raise HTTPException(400, "在庫が不足しています。") product.stock -= data.quantity order = Order(product_id=data.product_id, quantity=data.quantity) db.add(order) db.commit() return {"order_id": order.id} # 責務を分離した例 # service.py class OrderService: def __init__(self, db: Session): self.db = db def create_order(self, product_id: int, quantity: int) -> Order: product = self.db.query(Product).filter(Product.id == product_id).first() if not product: raise ProductNotFoundError(product_id) if product.stock < quantity: raise InsufficientStockError(product_id, product.stock, quantity) product.stock -= quantity order = Order(product_id=product_id, quantity=quantity) self.db.add(order) self.db.commit() return order # endpoint.py @app.post("/orders") async def create_order(data: OrderCreate, db: Session = Depends(get_db)): service = OrderService(db) try: order = service.create_order(data.product_id, data.quantity) except ProductNotFoundError: raise HTTPException(404, "商品が見つかりません。") except InsufficientStockError as e: raise HTTPException(400, f"在庫が不足しています(残り{e.available}個)。") return {"order_id": order.id} ``` 後者の構造では、`OrderService.create_order()` を HTTP を介さずに単体テストでき、同じロジックを管理コマンドやバッチ処理から再利用することも容易です。 LLM は前者のような一体型コードを生成しがちですが、責務の分離を意識して構造を整理する作業は開発者の判断に委ねられています。 ### 例外時にどうなるか 3つ目の問いは「正常系でなく異常系の挙動を自分は把握できているか」です。 LLM が生成するコードは正常系のパスに最適化されていることが多く、異常系への配慮が薄い傾向があります。 「ユーザーが正しい JSON を送り、データベースが正常に応答し、外部 API が 200 を返す」という前提でコードが書かれていると、そのどれかが崩れた瞬間にどうなるかが不明確です。 14-1 で学んだ例外の伝搬経路を思い出してください。 ビューで捕捉されなかった例外はミドルウェアへ、ミドルウェアで捕捉されなかった例外はフレームワークのハンドラへ、それでも処理されなかった例外はサーバ層へと伝わります。 この連鎖を理解していれば、「ここで例外が発生したとき、クライアントには何が返るか」を予測できます。 具体的に検証すべき異常系のパターンには、次のようなものがあります。 - **DB 接続切断**: どのような例外が送出され、それがどの層で捕捉されるか - **外部 API タイムアウト**: `httpx.TimeoutException` や `requests.Timeout` がビュー内で捕捉されているか、500 として伝搬するか - **Pydantic バリデーション**: FastAPI は `ValidationError` を自動的に 422 に変換するが、カスタムバリデータ内で `ValueError` 以外の例外を送出すると 500 になる ```python # 異常系を考慮していない例 @app.get("/weather/{city}") async def get_weather(city: str): async with httpx.AsyncClient() as client: resp = await client.get(f"https://api.weather.example/{city}") return resp.json() # 異常系を考慮した例 @app.get("/weather/{city}") async def get_weather(city: str): async with httpx.AsyncClient(timeout=5.0) as client: try: resp = await client.get(f"https://api.weather.example/{city}") resp.raise_for_status() except httpx.TimeoutException: raise HTTPException(504, "天気情報の取得がタイムアウトしました。") except httpx.HTTPStatusError as e: logger.warning(f"Weather API returned {e.response.status_code} for {city}") raise HTTPException(502, "天気情報サービスが一時的に利用できません。") return resp.json() ``` 例外時の挙動を確認する最も確実な方法は、実際に異常を発生させるテストを書くことです。 データベースのモックを接続エラーを返す状態に設定する、外部 API のモックをタイムアウトさせる、不正な JSON をリクエストボディとして送信する――これらのテストは LLM に生成させることもできますが、「どの異常を検証すべきか」の選定は本書で学んだ知識に基づく人間の判断です。 ### 本番で動くか 最後の問いは「このコードは本番環境の制約のもとで正しく動作するか」です。 ローカルの開発環境と本番環境の差異については 13-1 で体系的に整理しましたが、その差異はコードの正しさに直接影響します。 ローカルでは単一プロセスで動いているため発覚しない問題が、本番のマルチワーカー構成では即座に顕在化します。 最も典型的な例は、モジュールレベルの変数を使ったインメモリキャッシュです。 LLM が生成した「設定値をメモリにキャッシュして高速化する」コードは、単一プロセスでは期待どおりに動作しますが、Gunicorn の9ワーカー構成ではキャッシュがプロセスごとに独立するため、あるワーカーで更新したキャッシュが別のワーカーには反映されません。 12-6 で学んだプロセスモデルの知識がなければ、この問題の原因特定に長い時間を要するでしょう。 ファイルシステムへの書き込みも同様です。 一時ファイルを `/tmp` に書いて後続のリクエストで読み出すコードは、同じワーカーが処理すれば動作しますが、別のワーカーが処理した場合はファイルが見つかりません。 コンテナ環境ではさらに、Pod が再起動するとファイルシステム自体が初期化されます。 ```python # ローカルでは動くが本番で壊れる例 _config_cache = {} def get_config(key: str) -> str: if key not in _config_cache: _config_cache[key] = db_fetch_config(key) return _config_cache[key] # 本番で正しく動く例(Redis を使った共有キャッシュ) import redis cache = redis.Redis(host="redis", port=6379, db=0) def get_config(key: str) -> str: cached = cache.get(f"config:{key}") if cached is not None: return cached.decode() value = db_fetch_config(key) cache.set(f"config:{key}", value, ex=300) return value ``` 本番環境固有の制約はコードだけでなく設定にも及びます。 14-8 で詳しく解説したとおり、`DEBUG = False` への切り替え、HTTPS 環境でのプロキシヘッダ設定、CSRF と CORS の整合性、タイムアウトチェーンの一貫性など、ローカルでは検証しにくいが本番では致命的になる設定項目が多数あります。 ```{tip} 「本番で動くか」を確認するための実践的なアプローチとして、ステージング環境を本番と同一の構成(マルチワーカー、HTTPS、リバースプロキシ)で構築することをお勧めします。 Docker Compose で nginx、Gunicorn/Uvicorn、PostgreSQL、Redis を組み合わせたローカル本番環境を用意しておけば、上記の問題の多くをデプロイ前に発見できます。 ``` 以上の4つの問い――「どの層か」「どの責務か」「例外時にどうなるか」「本番で動くか」――は、LLM が生成したコードに限らず、自分自身が書いたコードや、コードレビューで他者のコードを読むときにも有効な観点です。 本書の14章分の知識は、これらの問いに具体的な根拠をもって答えるための土台です。 次節では、本書で学んだ内容を振り返り、ここから先の学習の道筋を提案します。 (AI に質問するときの分解法)= ## AI に質問するときの分解法 前節では「動くコード」を鵜呑みにしないための4つの問いを提案しました。 しかし LLM は「騙されないように警戒する相手」であると同時に、「うまく使えば強力な相棒」でもあります。 本節では視点を切り替え、LLM から質の高い回答を引き出すための質問の技術を扱います。 LLM に「動きません。助けてください」と投げかけても、有用な回答が返ってくることは稀です。 人間のシニアエンジニアに質問するときと同じで、問題を適切に分解し、必要な文脈を添えて伝えるほど、回答の精度は上がります。 そして問題を分解すること自体が、実はトラブルシューティングの大部分を占めています。 LLM への質問を組み立てる過程で、自分自身が問題の本質に気づくことも少なくありません。 ```{note} 問題を LLM に伝える前に分解する作業を行うと、多くの場合「分解の途中で自分で原因に気づく」という経験をします。質問の質を上げる訓練は、同時に自力でデバッグする力を高める訓練でもあります。 ``` 以降では、本書で学んだ Web アプリケーションの層構造を活かした4つの分解ステップを見ていきます。 ```{mermaid} flowchart TD P[問題発生] --> S1[層の切り分け
どの層で発生しているか] S1 --> S2[再現手順の言語化
環境 + 操作手順を記述] S2 --> S3[期待と実際の分離
何を期待し何が起きたか] S3 --> S4[ログとスタックトレースを添える
一次情報を提供] S4 --> AI[LLM への質問
精度の高い回答を得る] ``` ### まず層を切り分ける トラブルに遭遇したとき、最初にやるべきことは「問題がどの層で発生しているか」の切り分けです。 15-3 で確認した「どの層のコードか」という問いが、ここで質問の出発点になります。 本書を通じて見てきたとおり、リクエストはクライアント → リバースプロキシ → WSGI/ASGI サーバ → ミドルウェア → ルーティング → ビュー/エンドポイント → ORM → データベース という層を通過します。 「ページが表示されない」という症状は、このどの層でも発生し得ます。 - nginx が 502 を返している → プロキシとアプリサーバの接続の問題 - Django が 500 を返している → アプリケーションコードの問題 - ブラウザに白い画面が出る → 静的ファイルの配信の問題かもしれない 層を切り分けないまま LLM に質問すると、LLM は最も一般的な原因を推測するしかなく、的外れな回答が返りやすくなります。 たとえば「Django で 502 エラーが出ます」という質問に対して、LLM はビューのコード修正を提案するかもしれませんが、実際の原因は Gunicorn のワーカータイムアウトと nginx の `proxy_read_timeout` の不整合かもしれません。 層を特定してから質問すると、回答の精度が劇的に変わります。 ``` ❌ 悪い質問: 「Django アプリが本番で動きません」 ⭕ 層を特定した質問: 「nginx → Gunicorn (gthread, 4 workers, 4 threads) → Django 5.1 の構成で、 特定のエンドポイント (/reports/monthly/) へのリクエストだけが nginx のアクセスログで 504 を返しています。 他のエンドポイントは正常に 200 を返しています。 Gunicorn のエラーログには [CRITICAL] WORKER TIMEOUT (pid:1234) が 記録されています。 この問題は nginx と Gunicorn のどちらの設定を調整すべきですか?」 ``` 後者の質問は、問題がプロキシ層とアプリサーバ層の間にあること、特定のエンドポイントだけが影響を受けていること、ワーカータイムアウトが関係していることを明示しています。 LLM はこの情報から、13-4 で学んだタイムアウトチェーンの整合性や、12-8 のトラブルシューティング手法に相当する回答を返すことができます。 層の切り分けは次の3段階で行います。 1. nginx のアクセスログでステータスコードを確認する(4xx/5xx が nginx 由来なのかアプリ由来なのかを判別) 2. Gunicorn / Uvicorn のログを確認し、リクエストがアプリサーバに到達しているか、ワーカーがクラッシュしていないかを見る 3. アプリサーバに到達しているなら、アプリケーションのエラーログで例外の詳細を確認する この3段階のチェックを行ってから LLM に質問すれば、「どの層の問題か」がすでに絞り込まれた状態でスタートできます。 ### 再現手順を言語化する 層を特定したら、次に「どうすればその問題を再現できるか」を具体的に記述します。 再現手順の言語化は、LLM への質問の精度を高めるだけでなく、自分自身の理解を深める効果があります。 「なんとなくエラーが出る」を「この curl コマンドを実行するとこのエラーが出る」に変換する過程で、問題の条件が明確になるからです。 再現手順を書くときには、**環境の前提条件**と**操作手順**を分離することを意識してください。 ``` ⭕ 再現手順を含む質問の例: 【環境】 - Python 3.12 / Django 5.1 / Gunicorn 22.0 - PostgreSQL 16 (max_connections = 100) - Gunicorn: --workers 8 --threads 4 (= 最大32接続) - Celery ワーカー: 4プロセス (= 最大4接続) 【再現手順】 1. Apache Bench で同時接続50のリクエストを送る: ab -n 500 -c 50 https://example.com/api/orders/ 2. 約30秒後にレスポンスが返らなくなる 3. PostgreSQL のログに "FATAL: too many connections for role ..." が出る 4. Django のログに "OperationalError: connection to server ... failed" が出る 【質問】 Gunicorn 8 workers × 4 threads = 32 と Celery 4 = 合計36接続で PostgreSQL の上限100には余裕があるはずですが、なぜ接続が枯渇するのでしょうか。 CONN_MAX_AGE の設定が関係していますか? ``` この質問は12-6 のワーカー設計と12-8 の DB コネクション整合性の知識を背景にしています。 再現手順が具体的であるため、LLM は「`CONN_MAX_AGE` のデフォルトが0であり、リクエストごとに接続を開閉するが、高負荷時に接続のクローズが間に合わない場合がある」「スレッドごとに独立した接続が張られるため実際には32以上の瞬間接続が発生し得る」といった、12-8 で議論した内容に踏み込んだ回答を返せます。 ```{note} 再現手順を書けない場合は、それ自体が「問題をまだ十分に理解していない」ことの合図です。 その場合は LLM に「この症状の再現手順を特定するために、何を確認すべきですか」と聞くアプローチも有効です。 ``` ### 期待する挙動と実際の挙動を分ける 3つ目のステップは、「何が起きるべきか」と「実際に何が起きたか」を明確に区別して記述することです。 この区別は一見当たり前に思えますが、トラブル報告では驚くほど混同されます。 たとえば「CSRF エラーが出ます」という報告は実際の挙動しか伝えていません。 期待する挙動が「フォームの送信が成功して注文確認ページに遷移すること」なのか「AJAX リクエストが 200 を返すこと」なのかによって、調べるべき方向が変わります。 ``` ⭕ 期待と実際を分離した質問の例: 【期待する挙動】 React フロントエンド (https://app.example.com) から FastAPI バックエンド (https://api.example.com) への POST /api/orders/ が 201 を返し、注文が作成される。 【実際の挙動】 ブラウザの開発者ツールに以下のエラーが表示される: "Access to fetch at 'https://api.example.com/api/orders/' from origin 'https://app.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource." ネットワークタブを確認すると、OPTIONS リクエストが 401 を返している。 【現在の設定】 app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) 【質問】 CORSMiddleware は設定しているのに、 なぜ preflight の OPTIONS が 401 を返すのでしょうか? ``` この質問は14-8 で解説した CORS の誤解のパターンに正確に該当します。 LLM はこの情報から「認証ミドルウェアが CORS ミドルウェアより先に OPTIONS リクエストを処理して 401 を返している可能性がある」「ミドルウェアの登録順序を確認し、CORSMiddleware を最も外側に配置する必要がある」という回答にたどり着けます。 期待と実際を分離する習慣は、バグレポートの品質を上げるだけでなく、テストコードの設計にも直結します。 テストとは本質的に「期待する挙動をコードで記述し、実際の挙動と比較する」行為だからです。 ```{tip} 「期待する挙動」と「実際の挙動」を書き出す習慣は、バグレポートだけでなくテストの設計にもそのまま応用できます。`assert expected == actual` というテストの構造は、この思考パターンと完全に一致しています。 ``` ### ログとスタックトレースを添える 最後のステップは、言葉による説明だけでなく、機械が出力した事実を添えることです。 ログとスタックトレースは「何が起きたか」の客観的な記録であり、人間の解釈が入り込む余地のない一次情報です。 LLM にスタックトレースを渡すと、エラーの発生箇所、呼び出しチェーン、例外の種類と引数を正確に解析できます。 人間が「データベースエラーが出ました」と要約するよりも、`psycopg2.OperationalError: FATAL: too many connections for role "myapp"` というメッセージをそのまま渡すほうが、LLM は的確な回答を返せます。 ログを添える際には次の点を意識してください。 - **関連するログだけを抜き出す**: 数千行のアクセスログをそのまま貼り付けても LLM のコンテキストを圧迫するだけです。問題が発生した時刻の前後数十行、または `grep` でエラーレベルのログだけを抽出して渡しましょう - **複数の層のログを対応づける**: nginx のアクセスログのタイムスタンプとアプリケーションのエラーログのタイムスタンプを並べて渡すと、LLM はリクエストの流れを再構成できます ``` ⭕ ログを添えた質問の例: 問題の発生時刻: 2026-05-04 14:23:15 JST 【nginx アクセスログ】 14:23:15 "POST /api/orders/ HTTP/1.1" 502 0.052 【Gunicorn エラーログ】 [2026-05-04 14:23:15 +0900] [1234] [ERROR] Error handling request /api/orders/ Traceback (most recent call last): File "/app/myapp/views.py", line 45, in create_order order = service.create_order(data.product_id, data.quantity) File "/app/myapp/services.py", line 23, in create_order self.db.commit() ... sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) server closed the connection unexpectedly 【質問】 nginx は 502 を返し、Gunicorn のログでは DB 接続が 予期せず切断されています。 PostgreSQL のログには該当時刻のエラーはありません。 PgBouncer を経由しており、idle_timeout は 300秒に設定しています。 PgBouncer がアイドル接続を切断した可能性はありますか? ``` このように、ログとスタックトレースが添えられた質問は、LLM にとっても人間のシニアエンジニアにとっても最も回答しやすい形式です。 ```{warning} ログには機密情報が含まれることがあります。 外部の LLM サービスにログを送信する場合は、パスワード、API キー、個人情報が含まれていないかを確認し、必要に応じてマスクしてから送信してください。 ``` 以上の4ステップ――層の切り分け、再現手順の言語化、期待と実際の分離、ログの添付――は、LLM に限らずあらゆる技術的なコミュニケーションに通用するスキルです。 Stack Overflow への質問、チーム内のバグレポート、インシデント報告書、いずれも同じ構造で情報を整理すれば、回答者が問題を理解するまでの時間が短縮されます。 そして本書で14章にわたって積み上げてきた Web アプリケーションの内部構造への理解こそが、「どの層で何が起きているか」を正確に特定し、問いを的確に分解するための基盤です。 次節では、本書全体のまとめとして、ここから先の学習の指針を示します。 (AI の回答を検証するチェックリスト)= ## AI の回答を検証するチェックリスト 前節では LLM から質の高い回答を引き出すための質問の分解法を学びました。 しかし、どれほど精緻に質問を組み立てても、返ってきた回答をそのまま信用してよいとは限りません。 LLM は「もっともらしい回答」を生成することに長けていますが、プロジェクト固有の文脈やインフラ構成を完全に把握しているわけではありません。 以降では、LLM の回答を受け取った後に手早く実行できる4つの検証項目を見ていきます。 いずれも本書で積み上げてきた知識を直接活用するものであり、数分のチェックで致命的なミスを未然に防ぐことができます。 ```{mermaid} flowchart TD A[LLM の回答を受け取る] --> C1{開発用 / 本番用
DEBUG / SECRET_KEY} C1 --> C2{sync / async 整合
ブロッキング呼び出しはないか} C2 --> C3{WSGI / ASGI 前提
ワーカークラスは正しいか} C3 --> C4{セキュリティ
入力検証 / SQL / SSRF / リダイレクト} C4 --> OK[採用] ``` ### その設定は開発用か本番用か LLM が返すコードや設定の最も基本的な検証ポイントは、それが開発環境を前提としたものか本番環境を前提としたものかです。 14-7 で詳しく論じたとおり、この2つの環境は根本的に異なる要件を持っており、一方の設定をそのまま他方に持ち込むと障害やセキュリティインシデントの原因になります。 LLM が Django の設定例を返してきた場合、まず `DEBUG` の値を確認してください。 `DEBUG = True` のままのコードが返されることは珍しくありません。 LLM はしばしば「分かりやすさ」を優先して開発環境向けの設定を出力するためです。 同様に、`SECRET_KEY` がハードコードされていないか、`ALLOWED_HOSTS` が `["*"]` になっていないかを確認します。 FastAPI のコードでは、サーバの起動コマンドに注目します。 `uvicorn main:app --reload` が返ってきた場合、これは開発用です。 `--reload` は 15-2 で解説したとおりファイル変更の監視を行い、`--workers` との併用ができません。 本番では `gunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4` のような構成が必要です。 ```{caution} LLM が SQLite をデータベースとして使用するコードを返した場合、開発やプロトタイプとしては問題ありませんが、本番環境では同時接続の制約やデータの永続性の観点から PostgreSQL などへの移行が必要です。 Docker 環境では、コンテナ再起動時に SQLite のデータが消失するリスクもあります。 ``` 検証の具体的な手順として、LLM の回答に含まれる設定値を以下の基準で振り分けます。 ```python # 開発用の兆候(本番に持ち込んではいけない) DEBUG = True SECRET_KEY = "django-insecure-xxxxx" # ハードコードされた仮の値 ALLOWED_HOSTS = ["*"] # 全ホスト許可 DATABASES = {"default": {"ENGINE": "...sqlite3"}} # uvicorn main:app --reload # 本番で必要な設定 DEBUG = False SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # 環境変数から取得 ALLOWED_HOSTS = ["www.example.com"] # 明示的なホスト名 SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True # gunicorn myapp:app -k uvicorn.workers.UvicornWorker --workers 4 ``` LLM に「本番環境向けに修正してください」と追加で依頼すれば改善されることもありますが、最終的な確認は必ず自分の目で行ってください。 ```{tip} Django の `manage.py check --deploy` コマンドを使うと、本番環境向けの設定チェックを自動化できます。デプロイ前のルーティンチェックとして組み込むことをお勧めします。 ``` ### sync / async は整合しているか LLM が生成したコードの中で、同期処理と非同期処理が混在していないかを確認します。 12-7 で「async = 常に高速」が誤解であることを、15-2 で sync/async の違いが LLM に隠されやすいことを学びました。 ここではその知識を具体的な検証手順に変換します。 まず、エンドポイント関数の定義を確認してください。 `async def` で定義されたエンドポイント内で、同期的なライブラリを呼び出していないかが最も重要なチェックポイントです。 ```python # 要注意パターン: async def の中で同期ライブラリを使用 @app.get("/data") async def get_data(): response = requests.get("https://api.example.com/data") # 同期的 conn = psycopg2.connect(...) # 同期的 time.sleep(1) # 同期的 return {"data": response.json()} ``` このコードが本番でどうなるかは 12-7 で解説しました。 `async def` 内のブロッキング呼び出しはイベントループを停止させ、同じワーカーで処理中の他の全リクエストが巻き添えになります。 検証のチェックポイントとして、`async def` の中で使われているライブラリが非同期対応かどうかを確認します。 | 同期ライブラリ(危険) | 非同期対応への置き換え | |---|---| | `requests` | `httpx` (`AsyncClient`) | | `psycopg2` | `asyncpg` または `psycopg` (v3) の非同期モード | | `time.sleep()` | `asyncio.sleep()` | 置き換えが難しい場合は、エンドポイントを `def`(同期関数)として定義すれば、FastAPI がスレッドプールで実行してくれます。 Django 側でも同様の検証が必要です。 Django の ORM は基本的に同期的であり、ASGI デプロイ環境で `async def` ビューから ORM を直接呼び出すと `SynchronousOnlyOperation` 例外が発生します。 `sync_to_async` でラップするか、ビューを `def` のままにするかの判断が必要です。 LLM が Django の `async def` ビューを生成した場合は、ORM 呼び出しの有無を必ず確認してください。 ### WSGI / ASGI の前提は合っているか LLM はコードを生成する際に、そのコードが WSGI 環境で動くことを前提としているか ASGI 環境で動くことを前提としているかを明示しないことがあります。 この前提のズレは、一見すると正しいコードが実行時にまったく動かないという事態を引き起こします。 最も典型的なケースは、Django の WebSocket サポートや `async def` ビューが ASGI サーバ上でのみ動作するという点です。 LLM が Django Channels を使った WebSocket のコードを生成した場合、デプロイが `gunicorn myproject.wsgi:application` であれば WebSocket は一切機能しません。 `daphne myproject.asgi:application` や `uvicorn myproject.asgi:application` への変更が必要です。 逆に、FastAPI のコードを Gunicorn でデプロイする場合には、ワーカークラスとして `uvicorn.workers.UvicornWorker` を指定する必要があります。 LLM が `gunicorn myapp:app` とだけ返した場合、デフォルトの sync ワーカーが ASGI アプリケーションを処理しようとして失敗します。 | 構成 | 正しいコマンド | |---|---| | FastAPI (ASGI) | `gunicorn myapp:app -k uvicorn.workers.UvicornWorker` | | Django (WSGI, 同期ビューのみ) | `gunicorn myproject.wsgi:application` | | Django (ASGI, async ビュー/Channels) | `uvicorn myproject.asgi:application` | ```bash # 前提が合っていない例 gunicorn myapp:app # sync ワーカーでは ASGI アプリは動かない gunicorn myproject.wsgi:application # WSGI では WebSocket 不可 # 前提が合っている例 gunicorn myapp:app -k uvicorn.workers.UvicornWorker # FastAPI (ASGI) gunicorn myproject.wsgi:application # Django (WSGI, 同期ビューのみ) uvicorn myproject.asgi:application # Django (ASGI, async ビュー/Channels) ``` 検証の手順としては、LLM が返したコードの中からインターフェース規約を示すキーワードを探します。 Django であれば `wsgi.py` と `asgi.py` のどちらがエントリポイントとして参照されているか、FastAPI であれば起動コマンドに `-k uvicorn.workers.UvicornWorker` が含まれているか、ミドルウェアが WSGI 用(`__call__` メソッド)か ASGI 用(`__call__` が `async`)かを確認します。 また、使用しているライブラリの対応状況も重要です。 ASGI 環境で使うべき `httpx`(非同期 HTTP クライアント)と WSGI 環境で問題なく使える `requests`(同期 HTTP クライアント)の選択は、デプロイの前提と一致している必要があります。 LLM がこの整合性を自動的に担保してくれることは期待できないため、開発者が確認するようにしてください。 ### セキュリティ上の問題はないか 最後の検証項目は、14-4 から 14-6 にかけて学んだセキュリティの知識を動員した総合チェックです。 LLM が生成したコードに対して、次の6点を順に確認してください。 1. **入力バリデーション**: ユーザー入力がバリデーションなしに使われていないか - Django: フォームやシリアライザを経由せずに `request.POST` や `request.GET` の値を直接使っているコードは危険信号 - FastAPI: Pydantic モデルを経由せずに `await request.json()` で辞書を取り出しているコードも同様 2. **SQL インジェクション**: SQL クエリにユーザー入力が直接埋め込まれていないか - ORM の通常のメソッド(`filter()`, `get()`, `where()`)を使っていれば安全 - `raw()`, `extra()`, `text()` と f-string の組み合わせには SQL インジェクションのリスクがある 3. **SSRF**: 外部 URL へのリクエストにユーザー入力が使われていないか - `requests.get(user_provided_url)` のようなコードには内部ネットワークへのアクセスを拒否するバリデーションが必要 4. **オープンリダイレクト**: リダイレクト先にユーザー入力が使われていないか - `redirect(request.GET["next"])` のようなコードには `url_has_allowed_host_and_scheme()` による検証が必要 5. **ヘッダインジェクション**: レスポンスヘッダにユーザー入力が含まれていないか - `Content-Disposition` のファイル名等にユーザー入力が含まれる場合は改行文字の除去が必要 6. **秘密情報のハードコード**: API キー、DB パスワード、`SECRET_KEY` が文字列リテラルとしてコードに含まれていないか - LLM はサンプルとして `SECRET_KEY = "my-secret-key"` のような値を平然と出力するため、そのまま本番に持ち込まないよう注意が必要 ```python # LLM の回答に対するセキュリティチェックの要約 # 1. 入力バリデーション: Pydantic / Form を経由しているか? # 2. SQL: ORM を使っているか? raw SQL に f-string はないか? # 3. SSRF: ユーザー指定 URL にアクセスしていないか? # 4. リダイレクト: 遷移先を検証しているか? # 5. ヘッダ: ユーザー入力がヘッダに入っていないか? # 6. 秘密情報: ハードコードされていないか? ``` これら6つのチェックは数分で完了しますが、1つでも見落とすと本番環境でセキュリティインシデントにつながります。 LLM にコードレビューを依頼して二重チェックすることも有効ですが、LLM 自身が生成したコードの問題を LLM が指摘できない場合もあるため、最終判断は人間が行ってください。 ```{important} 以上の4つの検証項目――開発用か本番用か、sync/async の整合性、WSGI/ASGI の前提、セキュリティ――は、本書で学んだ知識の実践的な応用です。 LLM はコードの生成速度を飛躍的に向上させる道具ですが、生成されたコードの品質を保証する責任は開発者にあります。 ``` 次節では、本書全体のまとめとして学んだ知識の全体像を振り返ります。 (ジュニア開発者が最低限読めるようになりたいもの)= ## ジュニア開発者が最低限読めるようになりたいもの 前節までで、LLM に質問する技術と回答を検証するための観点を確認しました。 しかしこれらのスキルは、もう一段下の基礎の上に成り立っています。 それは「アプリケーションが出力するテキストを読む力」です。 Web アプリケーション開発においてコードを書く時間は全体の一部であり、残りの多くはコードが動いた結果として出力される情報を読み、解釈し、判断に変換する時間です。 LLM がコードを書いてくれる時代であっても、この「読む力」は自動化されません。 障害が起きたとき、最初に手がかりを与えてくれるのはログであり、スタックトレースであり、設定ファイルです。 以降では、ジュニア開発者が最低限「読める」ようになるべき5種類のテキストを取り上げ、それぞれの読み方の勘所を示します。 | テキストの種類 | 主な用途 | |---|---| | スタックトレース | 例外の発生箇所と呼び出しチェーンの把握 | | アクセスログ | リクエスト全体の傾向とステータスコードの確認 | | フレームワークエラーログ | アプリケーション内部の例外の詳細 | | サーバ起動ログ | 起動の成否とワーカー構成の確認 | | 設定ファイル | 意図された構成の把握と設定ミスの発見 | ### スタックトレース スタックトレースは、例外が発生した瞬間のプログラムの呼び出し履歴を上から下へ(Python の場合は最も古い呼び出しが最上部、例外の発生箇所が最下部に)表示したものです。 14-1 で例外の伝搬経路を学びましたが、スタックトレースはまさにその伝搬を逆方向にたどった記録です。 ```{tip} 読み方の鉄則は「下から読む」ことです。 最下行に例外の種類とメッセージがあり、その直上に例外が発生したファイル名・行番号・関数名があります。ここが問題の核心です。 ``` ``` Traceback (most recent call last): File "/app/venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/app/venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/app/myproject/orders/views.py", line 42, in create_order order = service.create_order(data["product_id"], data["quantity"]) File "/app/myproject/orders/services.py", line 28, in create_order self.db.commit() File "/app/venv/lib/python3.12/site-packages/sqlalchemy/orm/session.py", line 1923, in commit self._transaction.commit(_to_root=self.future) sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) server closed the connection unexpectedly ``` このスタックトレースを下から読むと、まず `sqlalchemy.exc.OperationalError` という例外が発生し、その原因は PostgreSQL の接続が予期せず切断されたことだと分かります。 発生箇所は SQLAlchemy の `session.commit()` ですが、それを呼び出したのは自分のコードの `services.py` の28行目 `self.db.commit()` であり、さらにその呼び出し元は `views.py` の42行目 `create_order` です。 スタックトレースの上半分にはフレームワークの内部コード(`django/core/handlers/` 等)が並びますが、通常はここに問題はありません。 注目すべきは自分のプロジェクトのコード(`/app/myproject/` 以下)がどの段階で現れるかです。 - フレームワーク内部のフレームしか含まれていない場合: 設定ミスやフレームワークのバグの可能性がある - 自分のコードのフレームがある場合: そこが調査の起点になる LLM にスタックトレースを渡して原因を聞くことは有効ですが、自分で下から3行を読んで「何が、どこで、なぜ起きたか」を把握してから質問するほうが、15-4 で学んだ「層の切り分け」が済んだ状態になるため、回答の精度が格段に上がります。 ### access log アクセスログは、サーバが受け取った全リクエストの記録です。 13-6 で詳しく扱いましたが、ここでは「読む」ことに焦点を当てます。 nginx の標準的なアクセスログは次の形式です。 ``` 203.0.113.50 - - [04/May/2026:14:23:15 +0900] "POST /api/orders/ HTTP/1.1" 502 0 "https://www.example.com/cart" "Mozilla/5.0 ..." 0.052 ``` 左から順に、クライアント IP、識別子(通常は `-`)、認証ユーザー(通常は `-`)、タイムスタンプ、リクエストメソッドとパスとプロトコル、ステータスコード、レスポンスサイズ、リファラ、ユーザーエージェント、処理時間(秒)です。 ジュニア開発者がまず注目すべきは次の3つの要素です。 1. **ステータスコード**: 200 番台は成功、301/302 はリダイレクト、4xx はクライアントエラー、5xx はサーバエラーです(14-2 で学んだステータスコードの知識がここで活きます) 2. **リクエストパス**: 特定のエンドポイントだけがエラーを返しているのか、全リクエストが影響を受けているのかで、問題の範囲が大きく変わります 3. **処理時間**: 通常は数十ミリ秒で応答しているエンドポイントが数秒かかっている場合、12-8 で学んだ CPU/I/O ボトルネックの切り分けに進みます アクセスログの実践的な読み方として、障害発生時にはまず `grep` でステータスコードを絞り込みます。 ```bash # 5xx エラーだけを抽出 grep '" 50[0-9] ' /var/log/nginx/access.log # 特定のエンドポイントの応答時間を確認 grep 'POST /api/orders/' /var/log/nginx/access.log | awk '{print $NF}' ``` ```{note} nginx のアクセスログと Gunicorn/Uvicorn のアクセスログの両方が有効になっている場合は重複が生じます。 13-6 で述べたとおり、通常は nginx 側のみを有効にし、アプリサーバ側は `--no-access-log` で無効化する運用が推奨されます。 ``` ### framework error log フレームワークのエラーログは、アプリケーション内部で発生した例外やエラーの詳細を記録したものです。 アクセスログが「何が起きたか」のサマリであるのに対し、エラーログは「なぜ起きたか」の詳細を提供します。 Django はデフォルトで `django.request` ロガーが 5xx エラーのスタックトレースを出力します。 `LOGGING` 設定でフォーマッタやハンドラを構成すれば、JSON 形式での出力やファイルへの出力が可能です。 ``` ERROR 2026-05-04 14:23:15 django.request Internal Server Error: /api/orders/ Traceback (most recent call last): ... sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) server closed the connection unexpectedly ``` エラーログを読む際の重要な視点は次の2点です。 - **時系列で追う**: 障害は単発のエラーから始まることもあれば、特定の時刻を境に急増することもあります。急増のパターンは外部サービスの障害・DB コネクション枯渇・メモリ不足など、インフラレベルの問題を示唆します。13-6 で学んだメトリクスの時系列データとエラーログの時刻を突き合わせると、因果関係が見えてきます - **同じ例外が繰り返されていないか**: 同じスタックトレースが数十回記録されている場合、根本原因は1つであり、それを解決すれば全てのエラーが消えます。異なるスタックトレースが混在している場合は、複合的な問題が発生しています ### server startup log サーバの起動ログは、アプリケーションが起動する過程で何が行われたかの記録です。 障害対応ではエラーログに注目しがちですが、「そもそもサーバが正しく起動しているか」を確認するために起動ログを読む能力は欠かせません。 Gunicorn の起動ログは次のような形式です。 ``` [2026-05-04 14:00:01 +0900] [1000] [INFO] Starting gunicorn 22.0.0 [2026-05-04 14:00:01 +0900] [1000] [INFO] Listening at: http://0.0.0.0:8000 (1000) [2026-05-04 14:00:01 +0900] [1000] [INFO] Using worker: uvicorn.workers.UvicornWorker [2026-05-04 14:00:01 +0900] [1001] [INFO] Booting worker with pid: 1001 [2026-05-04 14:00:01 +0900] [1002] [INFO] Booting worker with pid: 1002 [2026-05-04 14:00:01 +0900] [1003] [INFO] Booting worker with pid: 1003 [2026-05-04 14:00:01 +0900] [1004] [INFO] Booting worker with pid: 1004 ``` この起動ログから読み取れる情報を整理すると次のとおりです。 - Gunicorn のバージョン(22.0.0) - バインドアドレスとポート(`0.0.0.0:8000`) - ワーカークラス(`uvicorn.workers.UvicornWorker` であれば ASGI、`sync` や `gthread` であれば WSGI) - 起動したワーカーの数(4つ)とそれぞれの PID 12-6 で学んだワーカー設計の意図が、ここで具体的な数値として現れます。 起動ログで特に注意すべきは、ワーカーが起動直後にクラッシュしていないかです。 ``` [2026-05-04 14:00:02 +0900] [1001] [ERROR] Exception in worker process Traceback (most recent call last): ... ModuleNotFoundError: No module named 'myapp.settings_production' [2026-05-04 14:00:02 +0900] [1001] [INFO] Worker exiting (pid: 1001) [2026-05-04 14:00:02 +0900] [1000] [INFO] Booting worker with pid: 1005 ``` この場合、設定モジュールが見つからないためにワーカーがクラッシュし、アービタープロセスが新しいワーカーを起動するという無限ループに陥っています。 13-7 で学んだ `startupProbe` は、まさにこのような起動失敗を検知するための仕組みです。 Uvicorn の起動ログも同様に、バインドアドレス、ワーカー数、リロードモードの有無などを表示します。 ```{tip} `--reload` が有効になっている場合は `Started reloader process` という行が出ます。 本番環境で開発モードが誤って有効になっていないかを、起動ログで即座に判別できます。 ``` ### 設定ファイル 設定ファイルは、コードとインフラの接合点です。 Django の `settings.py`、FastAPI の環境変数設定、nginx の `nginx.conf`、Gunicorn の設定ファイル、Docker の `docker-compose.yml`、Kubernetes の `deployment.yaml` ――これらを読む力は、障害対応において「何が意図されていたか」を把握するために不可欠です。 Django の `settings.py` を読む際は、本書で繰り返し登場した次の設定項目に注目してください。 - `DEBUG` の値(14-7) - `ALLOWED_HOSTS` の内容(14-6) - `MIDDLEWARE` の順序(15-2) - `DATABASES` の接続設定と `CONN_MAX_AGE`(12-8) - `SECURE_PROXY_SSL_HEADER` と `SECURE_SSL_REDIRECT`(14-6, 14-8) - `CSRF_TRUSTED_ORIGINS`(14-8) これらの設定項目は、問題が発生した際に真っ先に確認すべき箇所です。 nginx の設定ファイルを読む際は、13-2 と 13-4 で学んだ内容が直結します。 `proxy_pass` のアドレスがアプリサーバの `--bind` と一致しているか、`proxy_read_timeout` と Gunicorn の `--timeout` の関係は 13-4 で解説した原則に従っているか、`proxy_set_header` で `X-Forwarded-For` と `X-Forwarded-Proto` が正しく設定されているかを確認します。 ```nginx # nginx.conf の読み方のポイント server { listen 443 ssl; # HTTPS でリッスン server_name www.example.com; # 許可するホスト名 proxy_connect_timeout 5s; # 接続タイムアウト(短め) proxy_read_timeout 60s; # 読取タイムアウト(Gunicorn --timeout より短く) proxy_set_header Host $host; # Host ヘッダ転送 proxy_set_header X-Forwarded-For $remote_addr; # クライアント IP proxy_set_header X-Forwarded-Proto $scheme; # プロトコル location / { proxy_pass http://127.0.0.1:8000; # Gunicorn の bind と一致するか } location /static/ { alias /var/www/myproject/staticfiles/; # collectstatic の出力先と一致するか } } ``` 設定ファイルの読解力は、一朝一夕に身につくものではありません。 しかし本書で学んだ各章の知識――ワーカー設計、リバースプロキシの役割、タイムアウトの連鎖、セキュリティ設定の意味――が「なぜこの設定値がここにあるのか」を理解するための地図になります。 以上の5種類のテキスト――スタックトレース、アクセスログ、フレームワークエラーログ、サーバ起動ログ、設定ファイル――は、Web アプリケーション開発者が日常的に読むことになるものです。 LLM にこれらの解釈を依頼することはできますが、15-4 で学んだとおり、LLM に質問する前にまず自分で「下から3行を読む」「ステータスコードで絞り込む」「起動ログで前提を確認する」という初動を行えるかどうかが、ジュニアとシニアを分ける境界線の一つです。 次節では、本書全体の旅路を振り返り、ここから先の学びへの道筋を示します。 (「直せる人」になるための習慣)= ## 「直せる人」になるための習慣 本書を通じて繰り返し強調してきたのは、「コードを書ける」ことと「問題を直せる」ことは別のスキルであるという事実です。 LLM がコードを書く速度を飛躍的に向上させた今、開発者の市場価値は「書く力」から「直す力」へと確実にシフトしています。 本番環境で発生した障害に対して、原因を特定し、修正し、再発を防止できる人は、どのチームでも替えのきかない存在です。 ```{note} 「直す力」は才能ではなく習慣の産物です。 毎回の障害を「外側から内側へ順番に調べる」という手順で取り組むだけで、ジュニア開発者でも着実に力をつけることができます。 ``` 以降では、障害に直面したときに外側から内側へと順に調べていく4段階の調査手順と、最終的に問題を切り出す「最小再現」の技法を、本書全体の知識を横断しながら見ていきます。 ```{mermaid} flowchart TD B[障害発生] --> H1["1. HTTP レベルで見る
curl でステータスコード確認"] H1 --> H2["2. サーバを見る
nginx / Gunicorn ログ確認"] H2 --> H3["3. フレームワークを見る
アプリエラーログ / 設定確認"] H3 --> H4["4. アプリコードを見る
スタックトレースと処理追跡"] H4 --> H5["最小再現を作る
問題を最小コードで再現"] ``` ### まず HTTP レベルで見る 障害報告を受けたとき、最初にすべきことはブラウザやアプリケーションコードの中に潜ることではありません。 まず、クライアントとサーバの間で何が起きているかを、HTTP の生のレベルで確認します。 `curl` はこの段階で最も信頼できる道具です。 ブラウザはキャッシュ・Cookie・JavaScript によるリクエスト書き換えなど多くの変数を持っていますが、`curl` はそれらを排除した純粋な HTTP リクエストを送信できます。 ```bash # ステータスコードとレスポンスヘッダを確認 curl -I https://www.example.com/api/orders/ # レスポンスボディも含めて確認(タイムアウトを設定) curl -v --max-time 10 https://www.example.com/api/orders/ # POST リクエストの検証 curl -X POST https://www.example.com/api/orders/ \ -H "Content-Type: application/json" \ -d '{"product_id": 1, "quantity": 3}' \ -v ``` `curl -v` の出力は、送信したリクエストヘッダ(`>` で始まる行)と受信したレスポンスヘッダ(`<` で始まる行)を表示します。 ここで確認すべき情報は次のとおりです。 - **ステータスコード**: 14-2 の知識をそのまま活用できます - **`Server` ヘッダ**: nginx か Gunicorn か Uvicorn かが分かります - **`Content-Type`**: JSON が返るべき場所で HTML が返っていないかを確認します - **レスポンス時間**: タイムアウトの問題をすぐに把握できます ステータスコードからは次に調べるべき層が決まります。 | ステータスコード | 示唆される問題 | |---|---| | 502 | リバースプロキシとアプリサーバの間の問題 | | 504 | アプリサーバの応答が遅すぎる | | 403 | CSRF や認証の問題 | | 200(でも画面が壊れている) | レスポンスの内容やフロントエンドの問題 | ブラウザの開発者ツールも HTTP レベルの観察に有用です。 Network タブでリクエストとレスポンスの詳細を確認し、Console タブで CORS エラーなどの JavaScript 側の問題を確認します。 14-8 で解説した CORS の誤解は、ほぼすべてこの段階で発見できます。 ### 次にサーバを見る HTTP レベルで問題の範囲が絞り込めたら、次にリバースプロキシとアプリケーションサーバのログを確認します。 これは本書の{numref}`開発環境と本番環境は何が違うのか`({ref}`開発環境と本番環境は何が違うのか`)で学んだ知識の実践です。 nginx のアクセスログとエラーログを確認してください。 15-6 で学んだとおり、アクセスログからはステータスコード、リクエストパス、処理時間を読み取り、エラーログからは具体的なエラーメッセージを取得します。 ```bash # nginx エラーログで直近のエラーを確認 tail -50 /var/log/nginx/error.log # 502/504 の発生頻度を確認 grep -c '" 502 \|" 504 ' /var/log/nginx/access.log ``` nginx のエラーログのメッセージから、次のように問題を絞り込めます。 - `connect() to 127.0.0.1:8000 failed (111: Connection refused)`: Gunicorn/Uvicorn が起動していないか、バインドアドレスが一致していない - `upstream timed out`: 13-4 で学んだタイムアウトチェーンの問題 次に Gunicorn / Uvicorn のログを確認します。 ```bash # Gunicorn のログ確認(systemd 管理の場合) journalctl -u gunicorn --since "10 minutes ago" # Docker 環境の場合 docker logs web --tail 100 --since 10m ``` Gunicorn のログに `[CRITICAL] WORKER TIMEOUT (pid:1234)` が記録されていれば、ワーカーが `--timeout` で設定した時間内に応答できなかったことを意味します。 この場合の調査方向は次の2つです。 - 特定のリクエストが長時間の処理を要しているか(CPU バウンドまたはスロークエリ) - ワーカーがデッドロックしているか 12-8 のトラブルシューティング手順がここで活きます。 ワーカーの稼働状況も確認します。 ```bash # Gunicorn ワーカーの状態確認 ps aux | grep gunicorn # メモリ使用量の確認 ps -eo pid,rss,command | grep gunicorn ``` 12-6 で学んだワーカー設計の知識があれば、ワーカー数が想定どおりか、メモリ消費が異常に増加していないか(メモリリークの兆候)を判断できます。 ```{tip} `--max-requests` が設定されていない環境でメモリが際限なく増加している場合は、ワーカーのリサイクルを導入する契機です。 `--max-requests 1000 --max-requests-jitter 50` のような設定で、ワーカーを定期的に再起動させることができます。 ``` ### 次にフレームワークを見る サーバ層に明らかな問題がなければ、調査対象をフレームワーク層に移します。 ここではアプリケーションのエラーログとフレームワークの設定を確認します。 15-6 で学んだフレームワークエラーログの読み方に従い、スタックトレースを下から読んで例外の種類と発生箇所を特定します。 例外の種類によって調査方向は大きく異なります。 | 例外の種類 | 示唆される問題 | |---|---| | `OperationalError` | データベース接続の問題 | | `PermissionDenied` | 認証・認可の問題 | | `ValidationError` | 入力データの問題 | | `ImproperlyConfigured` | 設定の問題 | フレームワーク層で特に確認すべきは、14-8 で詳しく扱ったセキュリティ設定起因の障害です。 - 全リクエストが 400 → `ALLOWED_HOSTS` を確認 - CSRF 検証失敗 → `CSRF_TRUSTED_ORIGINS` と `{% csrf_token %}` を確認 - 無限リダイレクト → `SECURE_PROXY_SSL_HEADER` を確認 これらの設定ミスは、フレームワーク層のログに明確な手がかりを残します。 Django であれば `manage.py shell` で設定値を直接確認することも有効です。 ```python # Django shell で設定を確認 from django.conf import settings print(settings.DEBUG) print(settings.ALLOWED_HOSTS) print(settings.SECURE_PROXY_SSL_HEADER) print(settings.DATABASES["default"]["HOST"]) ``` FastAPI であれば、`/docs` エンドポイント(本番で有効になっている場合)や、ヘルスチェックエンドポイントにデバッグ情報を一時的に含めることで、ランタイムの状態を確認できます。 ```{warning} 本番環境で `/docs` エンドポイントを有効にしたままにすることはセキュリティリスクです。 デバッグ目的で一時的に有効化する場合は、作業後に必ず無効化してください。 ``` ### 最後にアプリコードを見る 外側の3つの層を順に確認しても原因が特定できない場合、あるいは特定の層にまで原因が絞り込めた場合に、初めてアプリケーションコードの調査に入ります。 多くの開発者(特にジュニア)はこの順序を逆にして、最初からコードをデバッグしようとしますが、外側の層が壊れている場合にコードをいくら調べても原因は見つかりません。 ```{important} 「まずコードを見る」という習慣を持っていると、外側の層の問題を延々とコードで解決しようとするという時間の無駄が発生します。「外側から内側へ」という順序は、効率よく原因にたどり着くための設計原則です。 ``` コードを調査する際は、15-3 の「どの責務を担っているか」と「例外時にどうなるか」の問いが指針になります。 エラーが発生しているエンドポイントのコードを追い、データの流れを上流から下流へたどります。 リクエストデータの取得、バリデーション、ビジネスロジックの実行、データベース操作、レスポンスの構築という各段階で、どの段階まで正常に進み、どの段階で失敗しているかを `logger.debug()` やデバッガで確認します。 ```python # 段階的なログ出力で問題箇所を特定する @app.post("/api/orders/") async def create_order(data: OrderCreate, db: Session = Depends(get_db)): logger.debug(f"1. リクエスト受信: {data}") product = db.query(Product).filter(Product.id == data.product_id).first() logger.debug(f"2. 商品取得: {product}") if not product: raise HTTPException(404, "商品が見つかりません。") order = Order(product_id=data.product_id, quantity=data.quantity) db.add(order) logger.debug(f"3. コミット前") db.commit() logger.debug(f"4. コミット完了: order_id={order.id}") return {"order_id": order.id} ``` ログが「3. コミット前」まで出力されて「4. コミット完了」が出ていなければ、DB のコミット段階で問題が発生していることが分かります。 ここまで絞り込めれば、サーバ層のログに戻って DB 接続の状態を再確認するか、PostgreSQL のログを確認するかの判断ができます。 ```{tip} `logger.debug()` を使ったステップログは、開発中に有効にして本番では無効にする(ログレベルを `INFO` 以上に設定する)運用にしておくと、デバッグ時だけ詳細なログを有効化できて便利です。 ``` ### 最小再現を作る 問題の箇所が特定できたら、最後に行うべきは最小再現コードの作成です。 最小再現とは、問題を引き起こすために必要な最小限のコードと手順のことです。 最小再現を作る意義は次の3点です。 1. **問題の理解が深まる**: 余分なコードを削ぎ落とす過程で「この部分がなくても問題は再現する」「この部分を変えると問題が消える」という発見が生まれ、根本原因の理解が深まります 2. **修正の妥当性が担保される**: 最小再現コードで問題が再現することを確認し、修正を適用して問題が解消することを確認することで、修正が正しいことを証明できます 3. **情報共有の精度が上がる**: チームメイトや LLM に問題を伝える際、最小再現コードがあればあいまいさのない正確な情報共有ができます ```python # 最小再現の例: Gunicorn gthread ワーカーで DB コネクションが枯渇する問題 # reproduce.py from fastapi import FastAPI, Depends from sqlalchemy import create_engine, text from sqlalchemy.orm import Session, sessionmaker import time engine = create_engine("postgresql://user:pass@localhost/myapp", pool_size=2, max_overflow=0) SessionLocal = sessionmaker(bind=engine) app = FastAPI() def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.get("/slow") def slow_query(db: Session = Depends(get_db)): time.sleep(5) # 長時間処理をシミュレート result = db.execute(text("SELECT 1")) return {"result": result.scalar()} # 再現手順: # 1. gunicorn reproduce:app -k uvicorn.workers.UvicornWorker --workers 1 # 2. ab -n 10 -c 5 http://localhost:8000/slow # 3. pool_size=2 に対して同時5リクエストのため、3リクエストがコネクション待ちでタイムアウト ``` この最小再現は50行に満たないコードですが、12-6 のワーカー設計と 12-8 の DB コネクション整合性の問題を正確に再現しています。 `pool_size` を増やすか、`max_overflow` を設定するか、ワーカー数とプールサイズの関係を見直すかの判断に進めます。 最小再現を作る習慣は、LLM を活用する場面でも強力です。 15-4 で学んだ「再現手順を言語化する」の究極の形が最小再現コードであり、これを LLM に渡せば、層の切り分けも再現手順の記述も完了した状態で質問できます。 以上の5つのステップ――HTTP レベル、サーバ層、フレームワーク層、アプリコード、最小再現――を外側から内側へ順に実行する習慣が、「直せる人」を形作ります。 この手順は一見すると遠回りに思えるかもしれませんが、外側の層を飛ばしてコードに飛びつくよりも、結果的に速く原因にたどり着きます。 そして何より、本書の全14章で学んだ知識が、この手順の各段階で「次に何を確認すべきか」を教えてくれる羅針盤として機能します。 次節では、本書全体の旅路を振り返り、「フレームワークの向こう側」を読んだことで何が変わるかを考えます。 (本書のまとめ)= ## 本書のまとめ ここまで読み進めてくださった読者の皆さん、長い旅路にお付き合いいただきありがとうございます。 Vol.1「本書の対象読者とゴール」でブラウザから送信された HTTP リクエストの最初の一歩を追いかけ始めてから、次のようなテーマを一緒に探索してきました。 - WSGI と ASGI の仕組みとその違い - ミドルウェアの連鎖とその順序の意味 - ORM がクエリを組み立てる過程 - 並行処理のモデルとその選択基準 - 本番環境のデプロイ構成と開発環境との差異 - セキュリティの多層防御とその実装 - AI 時代の開発者の在り方とスキルセット 本節では、この旅路を3つの視点から振り返り、本書を閉じた後の歩みにつなげます。 ### 抽象化を使いこなすとは何か Django も FastAPI も、そして Python の Web エコシステム全体が、開発者を HTTP の生のバイト列やソケット操作から解放するために多層の抽象化を提供しています。 - `@app.get("/users/{user_id}")` と書けばルーティングが行われます - Pydantic モデルを定義すればバリデーションとシリアライズが自動的に処理されます - ORM のメソッドチェーンを書けば SQL が生成されてデータベースに送信されます - Django のフォームは CSRF トークンの生成と検証を透過的に行います - テンプレートエンジンはユーザー入力を自動的にエスケープします これらの抽象化は驚くべき生産性をもたらしますが、「抽象化を使う」ことと「抽象化を使いこなす」ことの間には大きな違いがあります。 抽象化を使うだけであれば、チュートリアルに書かれたコードを写し取ればよく、LLM に生成してもらうこともできます。 しかし抽象化を使いこなすとは、その抽象化が何を隠しているかを理解し、隠された層が問題を起こしたときに自分の手で掘り下げられることを意味します。 本書の前半で、Django のリクエスト処理を `WSGIHandler` から `BaseHandler._get_response()` まで追いかけたのは、`view` デコレータの裏側でどれだけの処理が行われているかを可視化するためでした。 FastAPI のルーティングを Starlette の `Router` クラスまで掘り下げたのは、`@app.get()` が何を登録しているかを理解するためでした。 ORM のクエリ生成を SQL レベルまで追跡したのは、`filter()` がどのような WHERE 句に変換されるかを確認できるようになるためでした。 抽象化の層を「必要に応じて一枚めくれる」能力は、障害対応においてかけがえのない武器になります。 - ORM が生成した SQL が意図しないフルテーブルスキャンを行っていることに気づくには、SQL レベルの理解が必要です - ミドルウェアの順序が原因で CSRF 検証が失敗していることを特定するには、ミドルウェアチェーンの実行順序の理解が必要です - Gunicorn のワーカータイムアウトと nginx の `proxy_read_timeout` の不整合が 502 を引き起こしていることを見抜くには、プロセスモデルとプロキシ構成の理解が必要です ```{note} 抽象化を使いこなすとは、抽象化を疑うことではありません。 Django の ORM を捨てて生 SQL を書くべきだとか、FastAPI の依存性注入を使わずに手動でインスタンスを管理すべきだということではありません。 フレームワークが提供する抽象化は、多くの場合において最良の選択です。 しかし、その抽象化がどの前提のもとで設計されているかを知り、前提が崩れたときに一段下の層で調査できること――それが「使いこなす」ということです。 ``` ### 実装を知ることが設計力につながる 本書で内部実装を追いかけた経験は、単なる知識の蓄積ではなく、設計判断の質を向上させます。 たとえば、{numref}`なぜ Web 開発で並行処理が重要なのか`({ref}`なぜ Web 開発で並行処理が重要なのか`)で並行処理のモデルを学んだ開発者は、新しいプロジェクトの技術選定において「このアプリケーションは CPU バウンドと I/O バウンドのどちらが主要なワークロードか」という問いを立てることができます。 I/O バウンドが支配的であれば FastAPI の非同期モデルが活き、CPU バウンドの処理が多ければ Gunicorn のマルチプロセスモデルが適しています。 この判断は、ワーカーの内部動作を理解していなければ「なんとなく FastAPI のほうが速そうだから」という根拠のない選択になってしまいます。 {numref}`開発環境と本番環境は何が違うのか`({ref}`開発環境と本番環境は何が違うのか`)でリバースプロキシとアプリケーションサーバの責務分離を学んだ開発者は、インフラ設計において次のような設計原則を自然に適用できます。 - 「静的ファイルは nginx に任せ、アプリサーバは API 処理に専念させる」 - 「タイムアウトは外側から内側へ短くなるように設定する」 - 「graceful shutdown のために各層の猶予時間を揃える」 {numref}`例外はどこで拾われるのか`({ref}`例外はどこで拾われるのか`)でセキュリティの多層防御を学んだ開発者は、機能の設計段階で次のような問いを自然に組み込めます。 - 「このエンドポイントはユーザー入力をどう検証するか」 - 「エラー発生時にどこまでの情報をクライアントに返すか」 - 「このリダイレクトは外部ドメインへの誘導に悪用されないか」 セキュリティは後から追加するものではなく、設計の段階で組み込むものだという感覚は、実装の詳細を知っているからこそ身につきます。 実装を知ることの効果はもう一つあります。 それは他者のコードを読む力です。 オープンソースのライブラリやフレームワークのソースコードは、世界でもトップクラスの開発者が書いた「設計の教科書」です。 Django の `WSGIHandler` がどのようにリクエストを処理するかを読んだ経験は、自分が書くコードの構造にも影響を与えます。 エラーハンドリングのパターン、設定の外部化、責務の分離――これらの設計原則は、フレームワークの実装を読むことで最も具体的に学ぶことができます。 ### 仕組みを理解したうえで AI を使う {numref}`AI 時代にジュニア開発者が直面する変化`({ref}`AI 時代にジュニア開発者が直面する変化`)を通じて、LLM との付き合い方を多面的に考えてきました。 - 15-1: AI 時代の変化 - 15-2: LLM が隠しがちなもの - 15-3: 「動くコード」を検証する観点 - 15-4: 質問の分解法 - 15-5: チェックリスト - 15-6: 読む力 - 15-7: 「直せる人」の習慣 これらを貫く一つのメッセージは、**LLM は「考える」ことの代替ではなく「実行する」ことの加速器である**ということです。 「Django でユーザー認証付きの REST API を作りたい」という要求に対して、LLM はトークン認証、権限管理、シリアライザ、ビューセットを含む一連のコードを数分で生成できます。 しかし、次のような問いに答えるのは、本書で学んだ知識を持つ開発者の仕事です。 - 「このアプリケーションのワークロード特性に対してワーカーは何プロセス何スレッドが適切か」 - 「DB コネクションプールのサイズはワーカー構成と整合しているか」 - 「CSRF と CORS の設定は SPA 構成で正しく機能するか」 - 「graceful shutdown 時にリクエストが欠落しないか」 LLM を最も効果的に活用できるのは、仕組みを理解している開発者です。 なぜなら、仕組みを知っていればこそ適切な問いを立てられ、返ってきた回答の妥当性を判断でき、必要に応じて修正を加えられるからです。 15-4 で示した「層を切り分けてから質問する」技法は、層構造の知識なしには実行できません。 15-5 で示した「sync/async の整合性を検証する」チェックは、並行処理モデルの理解なしには意味を持ちません。 本書のタイトル「フレームワークの向こう側」は、フレームワークが隠している仕組みの先に広がる世界を理解することが、真の開発力につながるという信念を表しています。 フレームワークの「向こう側」を覗くことは恐怖ではなく、むしろ自由をもたらします。 - フレームワークの動作原理を知っている開発者は、制約を理解したうえで最良の選択ができます - 本番環境の構成を理解している開発者は、障害に対して冷静に外側から調査を進め、原因を特定できます - セキュリティの仕組みを知っている開発者は、生成されたコードの脆弱性を見抜き、ユーザーを守れます 本書で得た知識は、技術の進化とともに詳細は変わっていくでしょう。 Django のバージョンは上がり、FastAPI も進化し、新しいフレームワークやツールが登場するかもしれません。 しかし、次のような原則は技術の変遷を超えて通用する知識です。 - HTTP の基礎 - プロセスとスレッドの概念 - 同期と非同期の違い - リバースプロキシの役割 - 入力値を信用しないという原則 - 外側から内側へ調査を進める手順 ```{admonition} ここから先の歩み方 :class: tip ここから先は、皆さん自身の旅路です。 本書が地図を広げたなら、実際にその土地を歩くのはあなた自身です。 - フレームワークのソースコードを読んでみてください - 本番環境のログを日常的に眺めてみてください - 障害が起きたときに、まず `curl` で HTTP レベルを確認する習慣を付けてみてください - LLM にコードを生成させた後に、本書のチェックリストを1分だけ実行してみてください その小さな習慣の積み重ねが、「動くコードを書ける人」から「正しく動くコードを設計し、壊れたときに直せる人」への道を切り開きます。 ``` フレームワークの向こう側は、覗けば覗くほど面白い世界が広がっています。 本書がその入口になれたなら、著者としてこれ以上の喜びはありません。