2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「フェイルセーフ(fail-safe)」と「フールプルーフ(fool-proof)」は、安全な設計を語るうえで頻出する2つの設計思想です。どちらも「事故を防ぐ」ための考え方ですが、守ろうとしているタイミングと相手が違います。この違いを曖昧にしたまま設計すると、「入力チェックは厳重なのに、障害時に最悪の状態で停止する」といったアンバランスなシステムになりがちです。

本記事では、2つの概念の違いを整理したうえで、Webアプリケーションやバックエンドで実際に使える実装パターンをコード付きで紹介します。元々は製造業・機械設計の用語ですが、ここではソフトウェア設計の文脈に翻訳して解説します。

対象読者

  • 「フェイルセーフ」と「フールプルーフ」をなんとなくで使い分けている方
  • 堅牢なシステム・APIを設計したいバックエンド/フロントエンドエンジニア
  • レビューで「ここはフェイルセーフに」と言われて言葉に詰まったことがある方

この記事でわかること

  • フェイルセーフとフールプルーフの本質的な違い
  • それぞれを実装に落とし込む具体的なパターン
  • フォールトトレランスなど関連概念との関係

TL;DR

ChatGPT Image 2026年6月15日 09_36_56.png

  • フールプルーフ = 壊さない・間違えさせない。人間の操作ミスを入口で防ぐ
    (バリデーション、型、UI制約)
  • フェイルセーフ = 壊れた後の安全。障害が起きる前提で、被害を最小化する
    (安全側デフォルト、タイムアウト、サーキットブレーカー)
  • フールプルーフは「事故が起きる前・対人間」、フェイルセーフは「事故が起きた後・対故障」と整理すれば混同しない
  • 両者は二者択一ではなく、多層防御として併用するのが正解

フェイルセーフとフールプルーフは何が違うのか

まず、それぞれの定義を確認します。

観点 フールプルーフ(fool-proof) フェイルセーフ(fail-safe)
何から守るか 人間の操作ミス・誤用 システム・部品の故障
いつ働くか 事故が起きる前(入口で防止) 事故が起きた後(被害を最小化)
基本思想 「間違った操作をできなくする」 「壊れても安全な状態で止まる」
身近な例 電子レンジは扉を開けると加熱が止まる/ギアがPでないとエンジンがかからない 石油ストーブは倒れると自動消火する/踏切は故障時に降りたままになる
ソフトの例 必須項目が空だと送信ボタンが押せない 認可サーバ応答不能時はアクセスを拒否する

ポイントは、フールプルーフは「ミスの発生」を防ぎ、フェイルセーフは「故障の影響」を防ぐことです。前者は完全には防ぎきれない(人は必ずミスをする)ため、後者がバックアップとして必要になります。

製造業の品質管理では、フールプルーフは「ポカヨケ(poka-yoke)」とも呼ばれます。トヨタ生産方式で体系化された考え方で、ソフトウェアの入力バリデーションと発想は同じです。

フールプルーフの実装パターン

「ユーザーに間違った操作をそもそもさせない」ためのパターンです。

パターン1: 不正な状態を表現できなくする(型で防ぐ)

最も強力なフールプルーフは、不正な値がコンパイル時点で存在できないようにすることです。実行時チェックより前に、型で弾きます。

src/domain/order.ts
// 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: 入力を境界で検証する(バリデーション)

外部入力は信頼できないため、システムの入口で一括検証します。スキーマバリデーションライブラリを使うと、検証と型付けを同時に行えます。

src/api/validate.ts
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 を効かせ、実行は明示フラグを要求する
  • 二重送信を防ぐため、送信中はボタンを非活性にする
src/cli/deploy.ts
function deploy(opts: { target: string; confirm: boolean }) {
  if (opts.target === "production" && !opts.confirm) {
    // 本番は --confirm なしでは実行できない
    throw new Error("本番デプロイには --confirm フラグが必要です");
  }
  // ...
}

フェイルセーフの実装パターン

「どれだけ防いでも障害は起きる」という前提に立ち、起きたときに安全な側へ倒すためのパターンです。

パターン4: デフォルトを安全側に倒す

判断に失敗したとき、どちらに転ぶかを設計で決めておきます。セキュリティ領域では「迷ったら拒否(fail closed)」が原則です。

auth/authorize.py
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: タイムアウトとリトライ

応答が返らない処理を無限に待つと、リソースを食いつぶして連鎖的に全体が停止します。待ち時間に必ず上限を設けるのがフェイルセーフの基本です。

src/lib/fetchWithTimeout.ts
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: グレースフルデグラデーション(縮退運転)

一部が壊れても全体を落とさず、機能を削って動き続ける設計です。

src/page/product.ts
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制約)
  • フールプルーフ: 取り消せない操作にガードがあるか
  • フェイルセーフ: 判断失敗時のデフォルトは安全側か(特に認可・決済)
  • フェイルセーフ: 外部呼び出しにタイムアウトがあるか
  • フェイルセーフ: 一部障害でも全体が落ちない縮退設計か
  • 例外を握りつぶして危険側に倒していないか

まとめ

  • フールプルーフは「事故が起きる前・対人間」、フェイルセーフは「事故が起きた後・対故障」の対策です。
  • フールプルーフは型・バリデーション・操作ガードで誤操作を入口から防ぎます。
  • フェイルセーフは安全側デフォルト・タイムアウト・サーキットブレーカー・縮退運転で、障害の影響を最小化します。
  • 人間のミスも機械の故障もゼロにはできません。だからこそ、両方を多層で組み合わせることが、堅牢なシステム設計の基本になります。

設計レビューで「ここはどう守る?」と問われたら、「入口(フールプルーフ)と出口(フェイルセーフ)の両方を見ているか」を思い出してみてください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?