背景
現在SIerで働いていますが、日々の業務では直接コードを書く機会が少ない環境にいます。
そのため、自己研鑽としてバックエンドの理解とGo言語の習得を目指して個人開発を始めました。
そして、ただ作るだけでなく、「なぜその設計にするのか」を自分の言葉で語れるように練習していきたい
今回のテーマ
開発中の「図書管理アプリ」において、最初の一歩として取り組んだエラー設計のリファクタリングについて記録します。
まだCRUDすら動いていない初期段階ですが、土台となるエラーの扱いがガタガタだと後で必ず詰むと思ったので、あえてこのタイミングで向き合ってみました。
現在のアプリ構成
- 言語:Go
- 機能:
- 書籍CRUD(現在は登録・検索のみ実装)
- ユーザ認識(未実装)
エラーはEntity層で定義し、各ハンドラで個別に判定している。
発生した問題:
現在の実装では、各メソッドで if err != nil を個別に判定し、
レスポンスを生成している。
output, err := h.useCase.CreateBook(r.Context(), input)
if err != nil {
// エラーが発生するたびに、ここで判定する必要があった
if errors.Is(err, domain.ErrInvalidBookTitle) || errors.Is(err, domain.ErrInvalidBookPrice) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 新しいエラーが増えるたびに、ここに else if が増えていく...
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
output, err := h.useCase.GetBookByID(r.Context(), id)
if err != nil {
if errors.Is(err, domain.ErrBookNotFound) {
http.Error(w, "Book not found", http.StatusNotFound)
return
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
この実装に対して以下の課題を感じた。
- 同じような if errors.Is(...) が各ハンドラーにコピペされる
- ステータスコードの判定ロジックが重複しまくり
- ハンドラーがドメイン層の詳細(具体的なエラー型)を知りすぎている
- レスポンス形式がバラバラで、APIとしての品質が安定しない
そこで、エラー内容を抽象化し、一括で処理できる仕組みを実装しました。
まず、ドメイン層に独自のAppError型定義します
type AppError struct {
Err error // 元のエラー
Code string // フロントエンドに返すための独自コード
ErrType ErrorType // NOT_FOUND, INVALID 等の分類
}
この「型」を持たせることで、ハンドラ側のコードは劇的にシンプルになりました。
output, err := h.useCase.CreateBook(r.Context(), input)
if err != nil {
RespondWithError(w, err)
return
}
output, err := h.useCase.GetBookByID(r.Context(), id)
if err != nil {
RespondWithError(w, err)
return
}
RespondWithError 内部で AppError を解析し、自動的に HTTP ステータスコード(404 や 400)へマッピングして JSON を返します。これにより、「エラーの判定ロジック」を一箇所に集約することができました。
func RespondWithError(w http.ResponseWriter, err error) {
var appErr *apperror.AppError
var status int
var code string
var message string
// 発生したエラーが AppError 型かどうかをチェック
if errors.As(err, &appErr) {
code = appErr.Code
message = appErr.Error()
// ErrorType を HTTP ステータスコードにマッピング
switch appErr.ErrType {
case apperror.TypeNotFound:
status = http.StatusNotFound
case apperror.TypeConflict:
status = http.StatusConflict
case apperror.TypeInvalid:
status = http.StatusBadRequest
default:
status = http.StatusInternalServerError
}
} else {
// AppError 以外(予期せぬエラー)の場合
status = http.StatusInternalServerError
code = "INTERNAL_SERVER_ERROR"
message = "予期せぬエラーが発生しました"
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(errorResponse{
Error: errorDetail{
Code: code,
Message: message,
},
})
}
最後に
まだCRUDすら完成していない段階ですが、この「エラーの整理」をしたことで、これからの開発が楽になりそうです。
「急がば回れ」じゃないですが、土台を固める楽しさを実感しています。
次はユーザー認証周りの実装していく予定です。そこでもまた新しい「気付き」を拾い集めて、形にしていきたいと思います!
開発は、まだまだ続きます!![]()