0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】Python × Clean Architecture × 例外処理

Last updated at Posted at 2025-11-18

はじめに

例外処理は「大規模アプリの設計品質」を決定づける核心部分。
Clean Architecture と組み合わせると、
“どの層で何が起きたか一瞬でわかり、修復しやすいアプリ” が作れる。

今回は、FastAPI を例にしながら、
Domain / UseCase / Repository / Infra / Presentation すべてが破綻しない例外設計
をまとめる。


結論

層ごとに例外を分離する

DomainError(業務)
UseCaseError(ユースケース)
RepositoryError(データ層)
InfraError(外部要因)

下位層 → 上位層へ例外を「翻訳」する(translating exceptions)

DBError → RepositoryError → DomainError → PresentationError
  • プレゼンテーション層(FastAPI)で最終的に HTTP 400 / 404 / 500 に変換

  • エラーログはすべて「infra 層」か「entrypoint」で記録

業務ロジックに print/log は絶対に置かない。


1. なぜ Clean Architecture に例外設計が必要?

Clean Arch の本質は:

「依存方向を内側へ統一して、変化に強い構造を作る」

だが、例外処理を正しく設計しないと…

  • どこでエラーが起きたのか判別できない
  • UseCase から DB の例外が飛んでくる
  • FastAPI が意図しない 500 を返す
  • ログがバラバラな場所に散る
  • エラー分類が曖昧で UI 層が扱えない

つまり、例外が体系化されていないだけで Clean Architecture が崩壊する


2. 理想的な例外階層(Error Hierarchy)

ここが最重要。

AppError
├── DomainError              (業務ロジック)
│     ├── ValidationError
│     ├── PermissionError
│     └── NotFoundError
│
├── UseCaseError             (ユースケースの失敗)
│
├── RepositoryError          (リポジトリ層)
│     ├── DBError
│     ├── DuplicateError
│     └── RecordNotFound
│
└── InfraError               (外部環境)
      ├── NetworkError
      ├── FileSystemError
      └── ExternalAPIError

ポイント

  • Infra と Repository の違い
    • Repository は「抽象化されたデータ操作の責務」
    • Infra は「OS/ネットワーク/外部APIなど外部環境」
  • DomainError は決して DB の例外を含まない(分離徹底)

3. データ層(Repository)での例外処理

「元の例外を包んで throw」するのが正解

NG(生の例外を外に漏らす)

def find_user(id):
    return db.fetch(id)  # DBError が上位に飛び出し CleanArch 崩壊

正しい書き方

from app.errors import RepositoryError, RecordNotFound

def find_user(id):
    try:
        row = db.fetch(id)
        if not row:
            raise RecordNotFound(f"user {id} not found")
        return row
    except DatabaseError as e:
        raise RepositoryError("DB failure") from e

Why?

上位層の UseCase は「DB が壊れたかどうか」を気にしない。
あくまで「Repository が失敗した」という抽象化された情報で十分。


4. UseCase 層の例外処理

RepositoryError → UseCaseError に翻訳する

from app.errors import UseCaseError, RepositoryError

class CreateUserUseCase:
    def execute(self, data):
        try:
            self.repo.insert(data)
        except RepositoryError as e:
            raise UseCaseError("ユーザー作成に失敗") from e

ポイント

  • UseCase は業務フローの責務を持つ
  • 詳細は知らず、抽象化されたエラーのみ扱う
  • ログはここで書かない(責務外)

5. ドメイン層(Entity/Domain Service)の例外扱い

ドメイン層では外部要因は扱わない。

DomainError だけ投げる

from app.errors import ValidationError

def create_user(name: str):
    if len(name) < 3:
        raise ValidationError("名前は3文字以上が必要です")

❌ NG:DatabaseError をこの層に入れる

絶対に層が汚染される。


6. Presentation 層(FastAPI)での例外処理

最後に “HTTP エラー” に変換する

例外ハンドラを作る:

from fastapi import FastAPI, HTTPException
from app.errors import DomainError, UseCaseError, RepositoryError

app = FastAPI()

@app.exception_handler(DomainError)
async def domain_error_handler(_, exc):
    return JSONResponse(
        status_code=400,
        content={"error": str(exc)}
    )

@app.exception_handler(UseCaseError)
async def usecase_error_handler(_, exc):
    return JSONResponse(
        status_code=422,
        content={"error": str(exc)}
    )

@app.exception_handler(RepositoryError)
async def repo_error_handler(_, exc):
    return JSONResponse(
        status_code=500,
        content={"error": "internal repository error"}
    )

Presentation 層ではじめて log を記録

logger.exception(exc)

ここなら責務的にも正しい。


7. 全体フロー図(Exception Flow)

もう少し詳細版:


8. 実例:User 作成の完全コード

errors.py(例外階層)

class AppError(Exception):
    pass

class DomainError(AppError):
    pass

class ValidationError(DomainError):
    pass

class UseCaseError(AppError):
    pass

class RepositoryError(AppError):
    pass

class RecordNotFound(RepositoryError):
    pass

class InfraError(AppError):
    pass

repository.py(データ層)

from app.errors import RepositoryError, RecordNotFound

class UserRepository:
    def find(self, user_id):
        try:
            row = db.fetch(user_id)
            if not row:
                raise RecordNotFound(f"user {user_id} not found")
            return row
        except DBError as e:
            raise RepositoryError("database failure") from e

usecase.py(ユースケース層)

from app.errors import UseCaseError, RepositoryError

class CreateUserUseCase:
    def __init__(self, repo):
        self.repo = repo

    def execute(self, data):
        try:
            return self.repo.insert(data)
        except RepositoryError as e:
            raise UseCaseError("ユーザー作成に失敗") from e

domain.py(ドメイン層)

from app.errors import ValidationError

def validate_user(name):
    if len(name) < 3:
        raise ValidationError("名前は3文字以上が必要です")

fastapi_entrypoint.py(プレゼンテーション層)

@app.post("/users")
def create_user(data: UserInput):
    usecase = CreateUserUseCase(repo)

    try:
        return usecase.execute(data)
    except AppError as exc:
        raise HTTPException(status_code=400, detail=str(exc))

まとめ

Clean Architecture × 例外処理の黄金原則(保存版)

1. 例外を下位層 → 上位層へ“翻訳”して渡す

2. 生のライブラリ例外を外に漏らさない

3.Domain には外部要因の例外を絶対に入れない

4. ログは Presentation 層(もしくは EntryPoint)で書く

5.例外階層は最初に設計し、チームで統一する

6. HTTP のエラーは最終的に Presentation 層で決定する

これができていると、
アプリ全体の「例外流」が非常に美しく、壊れにくくなる。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?