본문 바로가기
Backend2025년 6월 7일9분 읽기

Go 에러 래핑과 Sentinel Error 패턴

YS
김영삼
조회 769

Go 에러 핸들링 철학

Go는 예외(exception) 대신 에러 값을 반환하는 명시적 에러 처리를 채택했습니다. Go 1.13부터 에러 래핑(wrapping)이 표준에 추가되어, 에러에 컨텍스트를 더하면서 원본 에러를 보존할 수 있게 되었습니다. 이를 통해 에러 체인을 구성하고 errors.Is/As로 특정 에러를 검사할 수 있습니다.

Sentinel Error 패턴

package repository

import "errors"

// Sentinel Error 선언 — 패키지 수준의 고정 에러 값
var (
    ErrNotFound      = errors.New("record not found")
    ErrDuplicate     = errors.New("duplicate entry")
    ErrUnauthorized  = errors.New("unauthorized access")
    ErrInvalidInput  = errors.New("invalid input")
)

// 사용 예
func (r *UserRepo) FindByID(id int64) (*User, error) {
    user, err := r.db.QueryRow(ctx, query, id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrNotFound
        }
        return nil, fmt.Errorf("finding user %d: %w", id, err)
    }
    return user, nil
}

// 호출 측에서 Sentinel Error 검사
user, err := repo.FindByID(42)
if err != nil {
    if errors.Is(err, repository.ErrNotFound) {
        // 404 응답
        return c.JSON(404, map[string]string{"error": "user not found"})
    }
    // 500 응답
    return c.JSON(500, map[string]string{"error": "internal error"})
}

에러 래핑 (Error Wrapping)

package service

import "fmt"

// fmt.Errorf의 %w 동사로 에러 래핑
func (s *UserService) GetProfile(userID int64) (*Profile, error) {
    user, err := s.repo.FindByID(userID)
    if err != nil {
        // 컨텍스트 추가 + 원본 에러 보존
        return nil, fmt.Errorf("getting profile for user %d: %w", userID, err)
    }

    settings, err := s.settingsRepo.FindByUserID(userID)
    if err != nil {
        return nil, fmt.Errorf("loading settings for user %d: %w", userID, err)
    }

    return &Profile{User: user, Settings: settings}, nil
}

// 에러 체인 예시:
// "getting profile for user 42: finding user 42: record not found"
// errors.Is(err, ErrNotFound) -> true (체인 전체를 검사)

커스텀 에러 타입

// 구조화된 에러 타입
type AppError struct {
    Code    string  // "NOT_FOUND", "VALIDATION", "INTERNAL"
    Message string
    Field   string  // 유효성 검사용
    Err     error   // 래핑된 원본 에러
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

// Unwrap 구현으로 errors.Is/As 지원
func (e *AppError) Unwrap() error {
    return e.Err
}

// 생성자 함수
func NewNotFoundError(resource string, err error) *AppError {
    return &AppError{
        Code:    "NOT_FOUND",
        Message: fmt.Sprintf("%s not found", resource),
        Err:     err,
    }
}

func NewValidationError(field, message string) *AppError {
    return &AppError{
        Code:    "VALIDATION",
        Message: message,
        Field:   field,
    }
}

errors.Is와 errors.As 사용법

// errors.Is: 에러 체인에서 특정 값(Sentinel) 검사
if errors.Is(err, repository.ErrNotFound) {
    // ErrNotFound가 체인 어딘가에 존재
}

// errors.As: 에러 체인에서 특정 타입 검사 및 추출
var appErr *AppError
if errors.As(err, &appErr) {
    // appErr로 상세 정보 접근 가능
    switch appErr.Code {
    case "NOT_FOUND":
        return c.JSON(404, appErr)
    case "VALIDATION":
        return c.JSON(400, appErr)
    default:
        return c.JSON(500, appErr)
    }
}

// HTTP 핸들러에서 통합 에러 처리
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                slog.Error("panic recovered", "error", err)
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

에러 처리 모범 사례

원칙좋은 예나쁜 예
컨텍스트 추가fmt.Errorf("parsing config: %w", err)return err
에러 비교errors.Is(err, ErrNotFound)err.Error() == "not found"
로깅 위치최상위 핸들러에서 1회매 레이어에서 중복 로깅
에러 무시_ = file.Close() // 명시적file.Close() // 암묵적
// Go 1.20+: 다중 에러 래핑
func validateUser(u *User) error {
    var errs []error
    if u.Name == "" {
        errs = append(errs, NewValidationError("name", "이름은 필수입니다"))
    }
    if u.Email == "" {
        errs = append(errs, NewValidationError("email", "이메일은 필수입니다"))
    }
    if len(u.Password) < 8 {
        errs = append(errs, NewValidationError("password", "비밀번호는 8자 이상"))
    }
    return errors.Join(errs...)  // nil if no errors
}
  • Sentinel Error는 패키지의 공개 API에서만 선언하고, 내부에서는 에러 래핑을 사용하세요
  • 에러 문자열로 비교하지 말고 항상 errors.Is/As를 사용하세요
  • 에러 로깅은 에러를 최종적으로 처리하는 곳에서 한 번만 하세요

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록