Go 에러 핸들링 패턴 정리

문제 상황

마이크로서비스 API를 Go로 전환하면서 에러 처리가 일관되지 않아 디버깅에 시간이 많이 소요됐다. 특히 에러가 발생한 지점을 추적하기 어려웠다.

기본 패턴

Go 1.13부터 표준 라이브러리에 에러 wrapping이 추가됐지만, 스택 트레이스가 필요해서 github.com/pkg/errors를 사용했다.

import "github.com/pkg/errors"

func getUserData(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, errors.Wrap(err, "failed to find user")
    }
    return user, nil
}

커스텀 에러 타입

클라이언트에게 적절한 HTTP 상태 코드를 반환하기 위해 에러 타입을 분류했다.

type ErrorCode string

const (
    ErrNotFound   ErrorCode = "NOT_FOUND"
    ErrBadRequest ErrorCode = "BAD_REQUEST"
    ErrInternal   ErrorCode = "INTERNAL"
)

type AppError struct {
    Code    ErrorCode
    Message string
    Err     error
}

func (e *AppError) Error() string {
    return e.Message
}

미들웨어 레벨 처리

핸들러에서는 에러를 반환하고, 미들웨어에서 일괄 처리하는 방식으로 개선했다.

type Handler func(w http.ResponseWriter, r *http.Request) error

func ErrorMiddleware(h Handler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := h(w, r)
        if err != nil {
            log.Printf("%+v", err) // 스택 트레이스 출력
            
            var appErr *AppError
            if errors.As(err, &appErr) {
                respondError(w, appErr)
            } else {
                respondError(w, &AppError{
                    Code: ErrInternal,
                    Message: "Internal server error",
                })
            }
        }
    }
}

적용 결과

에러 발생 위치를 즉시 파악할 수 있게 됐고, 프로덕션 로그 분석 시간이 크게 줄었다. errors.Wrap을 사용하면 %+v 포맷으로 전체 스택을 확인할 수 있어 유용했다.

Go 에러 핸들링 패턴 정리