はじめに
この記事は、TRIAL&RetailAI Advent Calendar 2025 の 18 日目の記事です。
昨日は@zushi_ryotaさんの「1.26次元の図形?! ~フラクタル図形の次元を計算してみる~」という記事でした。雪の結晶にもみられるフラクタル図形を、D言語で生成しています。ぜひ、ご一読ください!
さて、エンジニアとして働き始めて2年が経ちました。
現在は、毎秒大量のデータが流れ込むリアルタイム処理システムの開発に携わっています。この2年間、数々の失敗を経験する中で「もっと早く知りたかった」と思うことがいくつもありました。
この記事では、私が身をもって学んだ教訓を共有します。
1. 仕様変更は必ずされる
「この仕様で確定です」は嘘です。
最初の頃、確定した仕様通りに「完璧な設計」をしたつもりでした。しかし現実は甘くありません。リリース前に「やっぱりこの項目も追加で」「計算ロジックを変更したい」が飛んできます。
// 変更に弱い設計:Serviceが直接PostgreSQLに依存
class OrderService(
private val postgresRepository: PostgresOrderRepository
) {
fun save(order: Order) {
postgresRepository.insert(order) // PostgreSQLに直接INSERT
}
}
「Google Cloudに保存先変更して」と言われた瞬間...
- ServiceクラスとRepositoryの両方を修正する羽目に
- 他にもPostgresRepositoryを使っている箇所を全て探して修正...
// 変更に強い設計:アクセス層を抽象化
interface OrderRepository {
fun save(order: Order)
}
class OrderService(
private val repository: OrderRepository // インターフェースに依存
) {
fun save(order: Order) {
repository.save(order) // 保存先を知らない
}
}
「Google Cloudに変更して」→ 実装クラスを差し替えるだけ
class PostgresOrderRepository : OrderRepository { ... }class GoogleCloudOrderRepository : OrderRepository { ... } // 追加するだけ
仕様変更に強い設計を心がけろ
- 「仕様は変わるもの」ということを前提に設計する
- ハードコーディングを避け、設定で変更できる余地を残す
- 拡張しやすい構造(インターフェース、Strategy パターン等)を意識する
設計については、@kakine_juriさんの
「機能別分解してるようじゃだめか。ライティングソフトウェアはね、履修しないと。」も参考にされてみてください。変動性に基づいて分解する設計方法の「Righting Software」についてとても分かりやすく説明してくれています。
2. データは必ずバリデーションしろ
変なデータは、必ず来る。
「仕様上ありえないデータ」は、現実には普通に飛んでくることが往々にしてあります。
実際に遭遇した例:
- 必須項目がnull、空文字
- 数値項目に負の値や桁あふれ
- 同一キーのデータが重複して到着
// 信じてはいけない
fun process(data: ExternalData) {
val amount = data.amount // nullかも、負の値かも、Intの最大値超えてるかも
}
// 入り口で弾く
fun process(data: ExternalData) {
val amount = requireNotNull(data.amount) { "amount is required" }
require(amount > 0) { "amount must be positive" }
}
外部からのデータは全て疑え
- 入り口で徹底的にバリデーション
- 異常データはログに残して弾く
- 「仕様上ありえない」は「現実にはありえる」
3. テストコードはちゃんと書け
大量データを扱うシステムでは、網羅できていないパターンでバグが起きる。
「正常系は動いた」で安心していたら、本番で特定の条件の組み合わせでのみ発生するバグに泣かされました。
// 見落としがちなテストケース
@Test
fun `空のリストでも正常に動作する`() { }
@Test
fun `大量データ(10万件)でもタイムアウトしない`() { }
@Test
fun `同一キーのデータが連続で来ても整合性が保たれる`() { }
@Test
fun `処理中にキューからのデータが途切れても復旧する`() { }
テストは未来の自分を守る保険
- 正常系だけでなく、境界値・異常系・大量データのテストも書く
- 「ありえない」と思うケースほどテストを書く
4. Gitは綺麗に管理しておけ
一時期、developブランチからfeatureブランチを切って、そのまま延々とコミットし続けていました。
結果どうなったか:
- どのコミットでバグが入ったか追跡不能
- レビューする側も地獄(差分が膨大すぎる)
- リリースの際に機能ごとで分解するのが大変
Gitは開発の生命線
- featureブランチは小さく、短命に
- コミットは「1つの変更につき1コミット」を意識
- コミットメッセージは「何をしたか」だけでなく「なぜしたか」も書く
- 定期的にdevelop/mainをマージして差分を小さく保つ
コミットメッセージについては、@fujithuroさんの「未来の自分を泣かせないコミットメッセージ」も参考にされてみてください。私も面倒くさくて、[fix]修正など意味のないコミットをしていた過去の自分を殴りたくなったことがありました...
5. 見積もりは長めに出しておけ
「3日でできます」→ 実際は1週間。
見積もりが甘くなる原因:
- 実装だけで考えて、テスト・レビュー・修正の時間を忘れる
- 「順調にいけば」の最短ルートで計算してしまう
- 他タスクの割り込みを考慮していない
- 調査や検証の時間を見積もっていない
見積もりは1.5〜2倍で出せ
- 「最短」ではなく「現実的」な見積もりを
- 不確実性が高いタスクは、その旨を伝えた上でバッファを積む
- 見積もりと実績を記録して、自分の精度を把握する
特にエンジニア歴が短い方は、3倍くらいで見積もっても怒られません(多分)。
6. データベースは万能じゃない
DBにも限界はある。
実際にハマった問題:
- INSERTを1件ずつ実行 → 大量データで激遅
- インデックスが最適化されていない → SELECTが数十秒かかる
- 1トランザクションに詰め込みすぎ → タイムアウト
// 遅い
items.forEach { repository.save(it) }
// 速い
repository.bulkInsert(items)
DBの特性を理解して使え
- 大量INSERTはbulk insertを使う
- 検索条件になるカラムにはインデックスを貼る
- 適切な単位でコミットし、長時間のトランザクションを避ける
- スロークエリログを監視する習慣をつける
7. よくわからずに使うな
「動いたからヨシ!」が一番危険。
コピペで動いたコード。なんとなく追加したアノテーション。理解せずに使ったライブラリ。これらは場合によっては、現場ネコ案件です。
実際にあった怖い話:
-
@Transactionalの挙動を理解せず使い、データ不整合が発生 - RabbitMQのackを理解せず、メッセージが消失
- 非同期処理の例外ハンドリング漏れで、エラーが闇に消えた
「なぜ動くのか」を説明できないコードは書くな
- 公式ドキュメントを読む習慣をつける(バージョンにも注意)
- 「とりあえず動いた」で終わらせず、挙動を確認する
- わからないことは聞く。今はAIにも気軽に聞けます。
8. ログはちゃんと残せ
障害発生時、ログがなければ原因究明は不可能。
「ログ出しすぎると重くなるかも」と遠慮した結果、本番障害で何が起きたか全くわからない地獄を見ました。
// ダメな例:何もわからない
fun process(data: Data) {
try {
service.execute(data)
} catch (e: Exception) {
throw e // 何のデータで失敗した?
}
}
// 良い例:追跡可能
fun process(data: Data) {
logger.info { "Processing started: id=${data.id}" }
try {
service.execute(data)
logger.info { "Processing completed: id=${data.id}" }
} catch (e: Exception) {
logger.error(e) { "Processing failed: id=${data.id}, payload=$data" }
throw e
}
}
ログは未来の自分への手紙
- 処理の開始・終了・失敗は必ずログに残す
- 「どのデータで」「何が起きたか」を特定できる情報を含める
- ただし、個人情報や機密情報はマスキングする
- ログレベル(INFO / WARN / ERROR)を適切に使い分ける
-
@WithSpanで適切にスパンを指定する(opentelemetry)
opentelemetryの使い方については、@kyojinnaapyonさんの「log.debug() だけでは見えなかった世界を OpenTelemetry が見せてくれた話」も参考にされてみてください。opentelemetryを活用できるようになることで、バグの特定が格段に早くなりました!
おわりに
2年間で学んだことを一言でまとめると、
「つまり、エンジニアは本当に面白い仕事だ。」
ということです。
AIに淘汰されない化け物エンジニアを目指しましょう。
明日のアドベントカレンダーは、@ikeda_takato さんの「ATT&CK T1059 深掘り:攻撃者がCLIを愛してやまない理由」です。お楽しみに!
RetailAIとTRIALではエンジニアを募集しています。
興味がある方はご連絡ください!