TechBlog一覧へ
プロジェクト引継ぎ 2026年5月15日

引き継いだコードを動かしたら、半年以上動いていない機能があった話

XECIN 技術的負債レガシーコード現場

システムの引き継ぎというのは、どこか「宝探しのような作業」だと思っています。

引継ぎドキュメントに書いてあることと、実際にコードが示すことが一致していないケースは珍しくない。私の経験では、ドキュメントの信頼度は最後に更新された日付に強く依存します。3年前が最終更新のドキュメントは、3年前の真実を記録しているわけです。

今回書くのは、そんな引き継ぎで出くわした話の中でも特に記憶に残っているケースです。

「ちゃんと動いています」

ある受注管理システムを引き継いだ時のことです。前任者からの引継ぎは、Wikiページ2枚とZIPで渡された「最新版」と書かれたソースコード一式でした。機能一覧には、受注登録・在庫照合・担当者へのメール通知の3機能が「稼働中」として記載されていました。

最初の確認作業として、ローカル環境を立ち上げ、受注登録を1件流してみました。画面上の処理は正常に終了し、在庫照合のステータスも「処理済み」に変わりました。ここまではドキュメント通りです。

「メール通知が来ない」と気づいたのは、登録から2〜3分後のことです。

最初は、ローカル環境のSMTP設定の問題だろうと思いました。Mailtrap等のダミー設定を使っていないかを確認し、実際の設定ファイルを見てみると、特に問題はありません。再度登録してみても、やはりメールは届かない。

コードを読んでいて、すぐに目についたのがこの箇所でした。

# メール送信処理(引継ぎ時のコード)
def send_notification(order_id, recipient_email):
    try:
        order = Order.query.get(order_id)
        msg = Message(
            subject=f"受注通知 #{order.id}",
            sender=current_app.config['MAIL_SENDER'],
            recipients=[recipient_email]
        )
        msg.body = render_template('mail/order_notification.txt', order=order)
        mail.send(msg)
        logger.info(f"Notification sent for order {order_id}")
    except Exception as e:
        logger.error(f"Mail send failed: {e}")
        # 処理はここで黙って終わる

except ブロックでエラーをログに出力しているのは良いとして、その後の処理が何もない。呼び出し元にも例外は伝播しません。メール送信に失敗しても、システムは「正常に処理した」と振る舞い続けるわけです。

ログが教えてくれた事実

本番環境のログにアクセスして、メール送信のレコードを確認しました。

grep "Notification sent for order" /var/log/app/application.log | tail -5

最後のヒットは、6ヶ月以上前の日付でした。

それ以降のログには、同じ時期から Mail send failed: Connection refused というエラーが大量に並んでいました。SMTP設定が変わったのか、外部メールサービスの認証情報が失効したのか、正確な原因はすぐには追えませんでした。ただ、この日を境にメール通知は完全に止まっていた。

ログを見た瞬間、正直なところ「6ヶ月も止まっていたのに、なぜ誰も気づかなかったんだ」という思いの方が先に来ました。受注担当者は毎日システムを使っているはずです。

誰も困っていなかった

受注担当の方に確認してみたところ、驚く回答が返ってきました。

「メール通知は、来てもほとんど見ていませんでした」

受注が入るとシステム上のステータスが変わるため、担当者は朝イチでシステムにログインして一覧を確認するのが習慣になっていたそうです。メール通知は「一応設定してある」程度の機能で、届いていてもスルー、届いていなくても気づかない——そういう状況が6ヶ月間続いていたわけです。

これはある意味で怖い話です。機能が止まっていても誰も困らなかったということは、「ビジネス的に不要な機能が動き続けていた(あるいは止まっていた)」か、「あっても困らない機能が静かに死んでいる」ことを意味します。単純に「壊れているから直す」ではなく、直す意味があるかどうかを確認してから修正する、という判断軸が必要でした。

今回は担当者と相談した上で「やはりあった方が便利」という結論になり、修正することにしました。

# 修正後のコード
def send_notification(order_id, recipient_email):
    try:
        order = Order.query.get(order_id)
        msg = Message(
            subject=f"受注通知 #{order.id}",
            sender=current_app.config['MAIL_SENDER'],
            recipients=[recipient_email]
        )
        msg.body = render_template('mail/order_notification.txt', order=order)
        mail.send(msg)
        logger.info(f"Notification sent for order {order_id}")
        return True
    except Exception as e:
        logger.error(f"Mail send failed for order {order_id}: {e}")
        # TODO: リトライ処理や代替通知(Slack連携等)を検討する余地がある。
        # 現状は失敗を呼び出し元に返すだけで、自動回復の仕組みはない
        return False

呼び出し元で戻り値を受け取って処理するようにしましたが、実はこの修正も完全ではありません。送信失敗を検知できるようになっただけで、失敗時のリカバリー設計は棚上げになっています。今思えば、最初のステップとして「まず状態を可視化する」だけで十分だったかもしれない。つい完成形を目指してしまうのは私の癖だったりします。

引継ぎ時のチェック項目を変えた

この経験を経て、引継ぎ時に「1サイクル流す」を必須項目にしました。

具体的には、受注登録・在庫照合・メール通知という1サイクルを、引継ぎを受ける段階で本番環境相当の環境で最初から最後まで実行します。そしてログをすべて確認する。「ドキュメントにこう書いてある」ではなく、「今この瞬間、システムはこういう動きをした」という事実を自分の目で見てから引き継ぐ、というルールです。

面倒に聞こえるかもしれませんが、半年のデッドを発見できるだけでなく、「動いているように見えてエラーが出ている箇所」を引継ぎ直後に見つけられるというメリットがあります。後から見つけるより、引継ぎ時に見つける方が「前任者の問題」として扱えますから、精神的にも楽です(そこが主目的ではありませんが)。

「ちゃんと動いています」という言葉を信じてはいけない、という話ではなくて。前任者も正直に言っていたつもりだったと思います。「動いている」の定義が、担当者とシステムでずれていることは珍しくない。1サイクル流してログを見るというのは、その定義のすり合わせをするための作業なんだと今は捉えています。

もっと良い引継ぎチェックの方法があれば、ぜひ教えてください。