はじめに
AIを使えば、以前よりはるかに速くWebアプリケーションを作れるようになりました。
エラーを貼れば原因を考えてくれる。
「こういう機能を追加したい」と伝えればコードを書いてくれる。
Dockerfileも、pytestも、認証処理も作ってくれる。
とても便利です。
ただ、個人開発を続けているうちに一つ気になることがありました。
自分が作ったこのシステムを、自分は本当に説明できるのだろうか?
コードが動くことと、その仕組みを理解していることは同じではありません。
そこで今回、自分が開発しているFlaskアプリについて、
コードを見ず、検索もせず、AIにも聞かず、自分の言葉だけで20問に答える
という理解度の棚卸しをしてみました。
結果として、かなり面白いことが分かりました。
対象にしたWebアプリ
今回棚卸ししたのは、個人開発しているベーカリー向け売上管理アプリです。
主な流れは、
商品を登録
↓
日々の販売数量を入力
↓
売上データを保存
↓
ランキング・グラフで可視化
↓
Gemini APIへ販売データを渡す
↓
経営改善の参考となる提案を表示
というものです。
主に使用している技術は、
- Python / Flask
- PostgreSQL
- SQLAlchemy
- Flask-Login
- Flask-WTF
- Alembic / Flask-Migrate
- Docker
- Gunicorn
- Render
- Gemini API
- pytest
- GitHub Actions
です。
機能としては動いています。
pytestも現在91件あります。
しかし今回確認したかったのは、
「使っている技術名を知っているか」ではなく、「なぜそこにあり、何をしているのか説明できるか」
でした。
ルールを決めた
今回は次の3段階で自己評価しました。
| 判定 | 基準 |
|---|---|
| 🟢 | 自分の言葉で説明できる |
| 🟡 | 方向は分かるが、一部曖昧・誤解がある |
| 🔴 | 説明できない、または理解が間違っている |
そして重要なのが、
最初からコードを見ない
ことです。
先にコードを見てしまえば、
「ここにこう書いてあるから、こういう意味です」
とは答えられます。
でも、それでは今の自分が理解しているかどうか分かりません。
そこでまず自分の理解だけで回答し、その後で実コードを確認することにしました。
20問で確認した
実際に確認したのは、次のような内容です。
| # | 質問 | 初回判定 |
|---|---|---|
| 1 | このシステムは誰の、どんな課題を解決するのか | 🟡 |
| 2 | ITに詳しくない店長へ30秒で説明できるか | 🟡 |
| 3 | Excel管理と何が違うのか | 🟡 |
| 4 | AIが使えなくなったら何が残るのか | 🟢 |
| 5 | 入力した売上データはどこへ行くのか | 🟡 |
| 6 | ブラウザからDBまでデータはどう流れるのか | 🔴 |
| 7 | GitHubは何のためにあるのか | 🟡 |
| 8 | 自分のPCを切ってもアプリが動くのはなぜか | 🟡 |
| 9 | Dockerはなぜ使っているのか | 🟢 |
| 10 | DockerとRenderは何が違うのか | 🟡 |
| 11 | GitHub・Render・PostgreSQLの役割の違い | 🟢 |
| 12 | SQLAlchemyは何をしているのか | 🔴 |
| 13 | ログイン機能は何を守っているのか | 🟡 |
| 14 | ページを移動してもログイン状態が続くのはなぜか | 🔴 |
| 15 | Geminiへ何を送っているのか | 🟡 |
| 16 | APIとは何か | 🔴 |
| 17 | APIキーをGitHubへ置いてはいけない理由 | 🟢 |
| 18 | 環境変数と.gitignoreの役割 |
🟡 |
| 19 | なぜpytestを91件まで増やしたのか | 🟢 |
| 20 | pytestが全件GREENならバグはないのか | 🟢 |
初回結果は、
🟢 6
🟡 10
🔴 4
でした。
今回は点数を競うことが目的ではありません。
むしろ、
どこで自分の理解が止まっているのかを見つける
ための診断として使いました。
利用者側の説明は比較的できた
興味深かったのは、
「このシステムを何のために作ったのか」については比較的説明できた
ことでした。
例えばAIについても、
AIがすべてを判断するシステムではなく、販売データをもとに店長が次の施策を考えるための参考意見を出すもの
という位置づけは理解できていました。
また、
AIが止まっても、商品登録・販売数量入力・ランキング・グラフなどの基本機能は使える
という説明もできました。
つまり、
利用者から見たシステムの目的
については、それなりに理解していました。
問題は、その裏側でした。
「GitHubを通ってDBへ行く」と思っていた
例えば、
ブラウザで入力した売上データは、どうやってPostgreSQLまで届くのか?
と聞かれたとき、最初は、
GitHubを通ってデータベースへ行く
というような説明をしていました。
これは間違いでした。
実際の流れは、
ブラウザ
↓
Flaskアプリ
↓
入力値のチェック
↓
SQLAlchemy
↓
PostgreSQL
です。
GitHubは、この通信経路には入りません。
現在の理解では、
GitHub
=設計書・ソースコード・変更履歴を保管する場所
Render
=Webアプリをインターネット上で動かす場所
PostgreSQL
=商品・販売数量などのデータを保管する倉庫
と分けています。
技術名は全部知っていました。
しかし、それぞれの役割を一本の流れとして説明できていなかったわけです。
Dockerも「完成したコンテナをRenderへ送る」と思っていた
Dockerについても誤解がありました。
私は当初、
自分のPCで作ったDockerコンテナを、そのままRenderへ送って動かしている
ようなイメージを持っていました。
しかし実際のDockerfileを一行ずつ確認していくと、理解が変わりました。
例えば、
FROM python:3.12-slim AS builder
では、Python 3.12 slimを土台としてbuilderステージを作っています。
さらに、
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
で必要なPythonパッケージを準備し、
FROM python:3.12-slim
でもう一度、本番用の新しいステージを作っています。
つまりマルチステージビルドです。
私はこれをミニ四駆に例えて理解しました。
builder
=組み立てピット
gcc / libpq-dev
=ニッパー・ヤスリなど組み立てに必要な工具
requirements.txt
=必要なパーツ一覧
本番ステージ
=実際にレースを走るマシン
組み立てに使った工具まで本番マシンへ積む必要はありません。
必要な完成部品だけ、
COPY --from=builder /root/.local /root/.local
で本番側へ移します。
そして、
COPY . .
によって、Docker build contextにあるアプリ本体を本番イメージへコピーします。
Dockerfileは、
完成した荷物そのものではなく、どう組み立て、どう積み込むかを書いた積付指示書
と考えると理解しやすくなりました。
SQLAlchemyは名前しか知らなかった
今回かなり大きかったのがSQLAlchemyです。
名前は何度も見ています。
実際、自分のコードにも大量に登場します。
しかし、
SQLAlchemyって何?
と聞かれたとき、最初は説明できませんでした。
実コードを見ると、
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
としてSQLAlchemyを用意し、
db.init_app(app)
でFlaskアプリへ接続しています。
売上保存では、
db.session.add(sale)
db.session.commit()
という処理があります。
現在は、
PostgreSQL
=データを保管する倉庫
SQLAlchemy
=Pythonとデータベースの間を取り持つ仲介役
と理解しています。
Alchemyは「錬金術」という意味なので、
Python側のデータを、データベースとやり取りできる形で扱いやすくしてくれるSQLの錬金術師
くらいのイメージで覚えることにしました。
さらに、
db.session.add()
=保存候補として出荷ラインへ配置
commit()
=この内容で正式確定
rollback()
=今回の未確定な変更を取り消す
という理解になりました。
厳密には、commit()より前にSQLがDBへ送られる場合もありますが、トランザクション全体としてはまだ確定前です。
以前はSQLAlchemyを「使っている技術一覧」に書くことはできても、ここまで説明できませんでした。
Cookieとキャッシュを混同していた
認証でも大きな誤解がありました。
私は最初、
ログイン情報はブラウザのキャッシュに保存されている
ような説明をしていました。
しかしキャッシュとCookieは別物です。
さらに実コードを確認すると、現在のmainではGoogle OAuthではなく、単一管理者ログインになっていました。
つまり、
「自分のアプリはGoogleログインを使っている」
という認識自体が、現在の実装とズレていました。
現在は役割を、
Cookie
=ブラウザ側で持つ入場券・入場バンド
Session
=「この利用者はログイン済み」などの状態
Flask-Login
=その状態を確認する入場管理スタッフ
@login_required
=「ここから先はログイン済みの人だけ」というゲート
と整理しています。
例えば、
@login_required
def dashboard():
となっていれば、
ログインしていない利用者が直接/dashboardへアクセスしても、Flask-Loginが認証状態を確認します。
また現在のコードでは、認証設定のフィンガープリントもSessionへ保存し、ログイン時の設定と現在の設定が一致するか確認する仕組みが入っています。
厳密には、Flask標準のSessionは必ずしも「サーバー側にある別の箱」ではありません。
デフォルトでは、署名されたCookieを使ってSession情報をブラウザ側に保持します。
ここでは役割を理解するために、
Cookie=入場券
Session=入場状態
と分けて考えています。
ここも、
「動いているから理解しているつもりだった場所」
でした。
APIとAPIキーも混ざっていた
APIについて聞かれたとき、最初は、
APIはキーみたいなもの
と答えてしまいました。
現在はこう整理しています。
API
=異なるアプリ・サービス同士がやり取りするための窓口
APIキー
=その窓口を利用するための認証情報
環境変数
=APIキーなどをコードとは別に管理する仕組み
実際のコードでは、
api_key = os.environ.get("GEMINI_API_KEY")
として環境変数からAPIキーを取得し、
client = genai.Client(api_key=api_key)
としてGeminiを利用する準備をしています。
つまり、
APIキーを使ってOSへ入る
のではありません。
os.environ.get()は、
アプリの実行環境に設定された環境変数から、
GEMINI_API_KEYという名前の値を取得する
処理です。
.gitignoreは「秘密を隠す機能」ではなかった
.gitignoreについても、
見られてはいけない変数を隠すもの
という理解でした。
これも少し違います。
.gitignoreは、
Gitに「このファイルは追跡対象へ入れないで」と指示する一覧
です。
現在の.gitignoreには、
過去売上高/
*.xlsx
*.db
.env
などがあります。
物流で例えるなら、
GitHub行きのトラックへ積んではいけない荷物の積載禁止リスト
です。
.gitignore自体が暗号化してくれるわけではありません。
さらに、一度Gitへ登録してしまった秘密情報は、後から.gitignoreへ追加しただけでは過去の履歴から消えません。
この違いも、説明してみて初めてはっきりしました。
Geminiへ実際に何を送っているのか
AI部分も確認しました。
私は最初、
dashboardに書いたプロンプトと売上数をGeminiへ送っている
と考えていました。
実際には、Gemini向けのプロンプト生成処理はprompts.pyへ分離されています。
def build_sales_prompt(sales_summary: str) -> str:
そしてapp.py側で、
sales_summary = ", ".join(
[f"{name}: {qty}個" for name, qty in ranked_sales]
)
prompt = build_sales_prompt(sales_summary)
として、
クロワッサン: 80個
食パン: 65個
メロンパン: 42個
のような商品名と販売数量をプロンプトへ組み込みます。
その後、
response = client.models.generate_content(
model=config.GEMINI_MODEL,
contents=prompt,
)
でGeminiへ送り、返ってきた文章を画面へ表示します。
つまり流れは、
PostgreSQL
↓
Flaskで売上集計
↓
商品名+販売数量
↓
prompts.pyで指示文を作成
↓
Gemini API
↓
提案を受け取る
↓
dashboardへ表示
です。
「SNSトレンドも分析している」は言い過ぎだった
ここで、今回かなり大きな発見がありました。
現在のプロンプトには、
近年の食トレンド
SNSでの訴求可能性
曜日・季節・祝日パターン
などを考慮するよう書いてあります。
私はそのため、
Geminiが現在のSNSやインターネット上のトレンドも調べて分析している
という感覚を持っていました。
しかし、実コードを確認すると、今回のAI分析処理からGeminiへ渡している店舗側の情報は、基本的に
商品名+集計された販売数量
です。
また、現在のこの処理にはWeb検索結果やSNSデータを取得して渡す実装もありません。
つまり、
プロンプトに「最近のトレンドを考慮して」と書くこと
と、
実際に現在のWeb情報を検索して分析すること
は別です。
さらに、もう一つ気づきました。
プロンプトでは、
曜日・季節・祝日パターンから読み取れる潜在需要
も分析観点に入れています。
しかし、AIアドバイス処理でGeminiへ渡している店舗データは、商品名と集計された販売数量が中心です。
曜日別・日別の詳細な販売データを、そのままGeminiへ渡しているわけではありません。
つまり、
「曜日を分析して」と指示すること
と、
曜日分析に必要なデータを渡していること
も別でした。
今回ここで学んだのは、
「検索して」とプロンプトに書く
≠
検索機能を実装している
「曜日を分析して」と書く
≠
曜日分析に十分なデータを渡している
ということです。
これは今回の棚卸しで最も分かりやすい、
「動いているシステムについて、自分が思い込んでいた機能と実装がズレていた例」
でした。
もし将来、本当に最新のWeb情報や曜日別の傾向まで含めた提案をしたいのであれば、
- Web検索機能を明示的に組み込む
- 日別・曜日別の販売データを渡す
- 分析対象の期間や条件を明確にする
といった追加設計が必要になりそうです。
pytest91件がGREENでも「安全」とは言えない
一方で、比較的説明できたのがpytestでした。
現在、このアプリには91件のpytestがあります。
なぜここまで増やしたのか。
私の場合は、
利用者は説明書通りに操作してくれるとは限らない
と考えているからです。
例えば、
- 存在しない商品IDを送る
- 別月の商品へ売上を登録する
- 販売終了商品へ売上を登録する
- 認証せずURLへ直接アクセスする
- CSRFトークンを改ざんする
- Gemini APIがエラーになる
- DB処理の途中で失敗する
といったケースです。
トラックの仕事でも、
「普通はこんなことをしないだろう」
だけで安全を考えることはできません。
何が起こるか分からないから、先に危険なパターンを考えます。
pytestも同じ感覚で、
事故が起こりそうなパターンを先に記録しておく「事故防止台帳」
として育てています。
ただし、
91 passed
だからといって、
バグが存在しない
という意味ではありません。
意味するのは、
現在テストしている91個の期待条件については、今回すべて通った
ということです。
想定していない穴が見つかれば、そこを修正し、新しいpytestを追加します。
そのため、以前はGREENを見ると安心していましたが、現在は、
「何を保証しているGREENなのか?」
を見るようになりました。
説明できないことは、恥ではなく「次に学ぶ場所」だった
今回、最初は少し恥ずかしさもありました。
自分で作ったアプリなのに、
- SQLAlchemyを説明できない
- Cookieとキャッシュを混同している
- APIとAPIキーを混同している
- GitHubが売上データの経路に入っていると思っている
- DockerコンテナをそのままRenderへ送っていると思っている
- 最新SNSトレンドまで取得していると思っている
といった穴が見つかったからです。
しかし途中から考え方が変わりました。
分からない場所が見つかったということは、次に学ぶ場所が見つかったということです。
むしろ怖いのは、
分かったつもりのまま使い続けること
だと思います。
「簡単に説明できる=完全理解」ではない
ここは注意したいところです。
例えば、
PostgreSQLは倉庫です。
と説明できたからといって、PostgreSQLを完全に理解したことにはなりません。
SQLAlchemyを仲介役と説明できても、内部実装まで理解したわけではありません。
今回やったことは、
説明できるかどうかを使って、理解の穴を探す
という方法です。
説明できることはゴールではありません。
理解度を調べる検査方法の一つ
だと考えています。
技術用語を使わない必要はない
また、説明するときに技術用語を完全に禁止する必要もないと思っています。
重要なのは、
相手が理解できる言葉へ翻訳できるか
です。
例えば、
PostgreSQL
=店舗データを保存する倉庫
SQLAlchemy
=PythonとDBを取り持つ仲介役
Dockerfile
=アプリをどう組み立てるかを書いた積付指示書
GitHub
=設計書と変更履歴の保管庫
Render
=アプリを動かすレンタルスペース
Cookie
=入場券
Session
=入場状態
Flask-Login
=入場管理スタッフ
API
=他サービスとやり取りする窓口
APIキー
=その窓口を利用するための認証情報
のように、
正式名称+自分が理解できる言葉
をセットにすると、かなり整理しやすくなりました。
自分で説明書を書けるかは、かなり良いテストになる
今回やってみて思ったのは、
自分のシステムの説明書を書けるか
という確認方法は意外と強いということです。
利用者向けなら、
何ができるのか
どう操作するのか
どんな場面で役立つのか
できないことは何か
を説明する必要があります。
開発者向けなら、
データはどう流れるのか
なぜこの技術を使っているのか
認証はどう成立しているのか
AIへ何を渡しているのか
障害時は何が残るのか
テストは何を保証しているのか
まで説明する必要があります。
書こうとして詰まった場所があれば、そこが理解不足の候補です。
さらに、
「この操作を説明するのがものすごく難しい」
となった場合、理解不足だけでなくUI/UX側に問題がある可能性もあります。
説明書を書くこと自体が、システム点検になるわけです。
AIを使わない方が良い、という話ではない
今回の記事は、
AIにコードを書かせるのは良くない
という話ではありません。
私は今後もAIを使います。
むしろ、AIがなければ短期間でここまで試行錯誤することは難しかったと思います。
ただ、
AIが作った
↓
動いた
↓
完成
で終わらせるのではなく、
AIが作った
↓
動いた
↓
なぜ動く?
↓
この部品は何をしている?
↓
壊したらpytestは検知する?
↓
利用者へ説明できる?
↓
実コードと自分の理解は一致している?
まで進めると、AIは単なるコード生成機ではなく、かなり強い学習道具になります。
まとめ
今回、自分のWebアプリを20問で棚卸ししてみました。
最初の結果は、
🟢 6
🟡 10
🔴 4
でした。
しかし重要だったのは点数ではありません。
実コードと照らし合わせたことで、
- GitHubは売上データの通信経路ではない
- Dockerfileは完成品ではなく組み立て指示書
- SQLAlchemyはPythonとDBをつなぐ役割を持つ
- Cookie・Session・Flask-Loginはそれぞれ役割が違う
- APIとAPIキーは別物
-
.gitignoreは暗号化機能ではない - Geminiへ実際に送っているデータを確認できた
- 「最新SNSトレンドも分析している」という自分の認識と実装にズレがあった
- 「分析してほしい内容」と「実際に渡しているデータ」が一致しているとは限らない
という理解の穴を発見できました。
そして今回一番大きかったのは、
「知らなかったこと」より、「知っているつもりだったこと」の方が危ない
と実感したことです。
AI時代は、コードを作る速度そのものはどんどん上がっていくと思います。
だからこそ、
これは何のためにあるのか?
どうやって動いているのか?
壊れたらどうなるのか?
利用者へ説明できるのか?
を自分の言葉で確認する時間は、以前より重要になるのかもしれません。
今後も、
「動いた」で終わらせず、「説明できる」まで持っていく。
これを意識しながら個人開発を続けていこうと思います。
pytest強化シリーズ
