はじめに
「フェイルセーフ(fail-safe)」と「フールプルーフ(fool-proof)」は、安全な設計を語るうえで頻出する2つの設計思想です。どちらも「事故を防ぐ」ための考え方ですが、守ろうとしているタイミングと相手が違います。この違いを曖昧にしたまま設計すると、「入力チェックは厳重なのに、障害時に最悪の状態で停止する」といったアンバランスなシステムになりがちです。
本記事では、2つの概念の違いを整理したうえで、Webアプリケーションやバックエンドで実際に使える実装パターンをコード付きで紹介します。元々は製造業・機械設計の用語ですが、ここではソフトウェア設計の文脈に翻訳して解説します。
対象読者
- 「フェイルセーフ」と「フールプルーフ」をなんとなくで使い分けている方
- 堅牢なシステム・APIを設計したいバックエンド/フロントエンドエンジニア
- レビューで「ここはフェイルセーフに」と言われて言葉に詰まったことがある方
この記事でわかること
- フェイルセーフとフールプルーフの本質的な違い
- それぞれを実装に落とし込む具体的なパターン
- フォールトトレランスなど関連概念との関係
TL;DR
-
フールプルーフ = 壊さない・間違えさせない。人間の操作ミスを入口で防ぐ
(バリデーション、型、UI制約) -
フェイルセーフ = 壊れた後の安全。障害が起きる前提で、被害を最小化する
(安全側デフォルト、タイムアウト、サーキットブレーカー) - フールプルーフは「事故が起きる前・対人間」、フェイルセーフは「事故が起きた後・対故障」と整理すれば混同しない
- 両者は二者択一ではなく、多層防御として併用するのが正解
フェイルセーフとフールプルーフは何が違うのか
まず、それぞれの定義を確認します。
| 観点 | フールプルーフ(fool-proof) | フェイルセーフ(fail-safe) |
|---|---|---|
| 何から守るか | 人間の操作ミス・誤用 | システム・部品の故障 |
| いつ働くか | 事故が起きる前(入口で防止) | 事故が起きた後(被害を最小化) |
| 基本思想 | 「間違った操作をできなくする」 | 「壊れても安全な状態で止まる」 |
| 身近な例 | 電子レンジは扉を開けると加熱が止まる/ギアがPでないとエンジンがかからない | 石油ストーブは倒れると自動消火する/踏切は故障時に降りたままになる |
| ソフトの例 | 必須項目が空だと送信ボタンが押せない | 認可サーバ応答不能時はアクセスを拒否する |
ポイントは、フールプルーフは「ミスの発生」を防ぎ、フェイルセーフは「故障の影響」を防ぐことです。前者は完全には防ぎきれない(人は必ずミスをする)ため、後者がバックアップとして必要になります。
製造業の品質管理では、フールプルーフは「ポカヨケ(poka-yoke)」とも呼ばれます。トヨタ生産方式で体系化された考え方で、ソフトウェアの入力バリデーションと発想は同じです。
フールプルーフの実装パターン
「ユーザーに間違った操作をそもそもさせない」ためのパターンです。
パターン1: 不正な状態を表現できなくする(型で防ぐ)
最も強力なフールプルーフは、不正な値がコンパイル時点で存在できないようにすることです。実行時チェックより前に、型で弾きます。
// Before: status は何でも入る。"shippped" のようなタイポも通ってしまう
interface OrderBad {
status: string;
}
// After: 取りうる値を型で固定。不正な文字列はコンパイルエラー
type OrderStatus = "pending" | "paid" | "shipped" | "delivered";
interface Order {
status: OrderStatus;
}
function ship(order: Order) {
// order.status が "shippped" だとそもそも代入できない
}
メリット:
- レビューやテストに頼らず、コンパイラが誤用を検出する
- 取りうる値が型から自明になり、ドキュメントを兼ねる
パターン2: 入力を境界で検証する(バリデーション)
外部入力は信頼できないため、システムの入口で一括検証します。スキーマバリデーションライブラリを使うと、検証と型付けを同時に行えます。
import { z } from "zod";
const SignupSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
});
export function parseSignup(input: unknown) {
// 不正なら例外。以降のコードは「検証済みの型」だけを扱える
return SignupSchema.parse(input);
}
バリデーションは「フロントエンドだけ」では不十分です。フロントの制約はユーザー体験のため、サーバ側の検証はセキュリティのため、と役割が異なります。必ず両方に実装してください。フロント側の制約は開発者ツールやAPI直叩きで簡単に回避できます。
パターン3: 危険な操作にガードを設ける
取り消せない操作(削除、課金、本番デプロイ)は、ワンクリックで実行できないようにします。
- 削除前に対象名の入力を要求する(GitHubのリポジトリ削除方式)
- 破壊的コマンドにデフォルトで
--dry-runを効かせ、実行は明示フラグを要求する - 二重送信を防ぐため、送信中はボタンを非活性にする
function deploy(opts: { target: string; confirm: boolean }) {
if (opts.target === "production" && !opts.confirm) {
// 本番は --confirm なしでは実行できない
throw new Error("本番デプロイには --confirm フラグが必要です");
}
// ...
}
フェイルセーフの実装パターン
「どれだけ防いでも障害は起きる」という前提に立ち、起きたときに安全な側へ倒すためのパターンです。
パターン4: デフォルトを安全側に倒す
判断に失敗したとき、どちらに転ぶかを設計で決めておきます。セキュリティ領域では「迷ったら拒否(fail closed)」が原則です。
def is_allowed(user, resource) -> bool:
try:
return policy_engine.check(user, resource)
except Exception:
# 認可エンジンが落ちたら「許可」ではなく「拒否」に倒す
logger.error("authorization check failed; denying access")
return False # fail closed
ここで return True(fail open)にすると、認可システムの障害がそのまま不正アクセスにつながります。握りつぶして許可側に倒すのは最も危険なアンチパターンです。例外をログに残し、安全側(拒否)で停止させてください。
ただし、安全側がつねに「拒否」とは限りません。何が「安全」かはドメイン次第です。
| システム | 安全側(fail-safe な状態) |
|---|---|
| 認可・決済 | 拒否する(fail closed) |
| 火災報知器・非常口 | 開放する・鳴らす(fail open) |
| 信号機 | 全方向赤、または黄点滅 |
パターン5: タイムアウトとリトライ
応答が返らない処理を無限に待つと、リソースを食いつぶして連鎖的に全体が停止します。待ち時間に必ず上限を設けるのがフェイルセーフの基本です。
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
パターン6: サーキットブレーカー
障害中のサービスを叩き続けると、相手の回復を妨げ、自分も詰まります。一定回数失敗したら一時的に呼び出しを遮断し、即座にフォールバックへ切り替えます。
- Closed: 通常どおり呼び出す
- Open: 呼び出さず即エラー(またはフォールバック)を返す
- HalfOpen: 様子見で少数だけ通し、回復していれば Closed に戻す
パターン7: グレースフルデグラデーション(縮退運転)
一部が壊れても全体を落とさず、機能を削って動き続ける設計です。
async function getProductPage(id: string) {
const product = await getProduct(id); // これは必須
// レコメンドは「あれば嬉しい」程度。失敗してもページは出す
let recommendations: Product[] = [];
try {
recommendations = await getRecommendations(id);
} catch (e) {
logger.warn("recommendation unavailable, showing page without it", e);
}
return { product, recommendations };
}
縮退運転では「何が必須で、何が省略可能か」をあらかじめ決めておくことが重要です。すべてを必須にすると、些細な依存先の障害で全体が落ちます。
関連概念との整理
フェイルセーフの周辺には似た用語が多く、混乱しがちです。違いを整理します。
| 用語 | 意味 | フェイルセーフとの関係 |
|---|---|---|
| フェイルセーフ | 故障時に安全な状態で停止する | 本記事の主題 |
| フェイルソフト | 故障時に機能を縮退して動作継続する | フェイルセーフの一種(パターン7) |
| フェイルオーバー | 故障時に予備系へ自動切替する | 可用性を保つ手段 |
| フォールトトレランス | 故障が起きても正常に動き続ける | より上位の目標概念 |
| フールプルーフ | そもそも誤操作をさせない | 故障前・対人間の対策 |
ざっくり言えば、フォールトトレランス(耐障害性)という大きな目標があり、それを実現する手段としてフェイルセーフ・フェイルソフト・フェイルオーバーがある、という関係です。フールプルーフだけは「故障」ではなく「誤操作」を対象にする点で系統が異なります。
設計時のチェックリスト
機能を設計するとき、次の観点で「抜け」がないか確認すると、片側だけに偏った設計を防げます。
- フールプルーフ: 不正な入力・操作を入口で弾いているか(型・バリデーション・UI制約)
- フールプルーフ: 取り消せない操作にガードがあるか
- フェイルセーフ: 判断失敗時のデフォルトは安全側か(特に認可・決済)
- フェイルセーフ: 外部呼び出しにタイムアウトがあるか
- フェイルセーフ: 一部障害でも全体が落ちない縮退設計か
- 例外を握りつぶして危険側に倒していないか
まとめ
- フールプルーフは「事故が起きる前・対人間」、フェイルセーフは「事故が起きた後・対故障」の対策です。
- フールプルーフは型・バリデーション・操作ガードで誤操作を入口から防ぎます。
- フェイルセーフは安全側デフォルト・タイムアウト・サーキットブレーカー・縮退運転で、障害の影響を最小化します。
- 人間のミスも機械の故障もゼロにはできません。だからこそ、両方を多層で組み合わせることが、堅牢なシステム設計の基本になります。
設計レビューで「ここはどう守る?」と問われたら、「入口(フールプルーフ)と出口(フェイルセーフ)の両方を見ているか」を思い出してみてください。
