Go errors 패키지 활용 – 업무에 사용하는 Go 언어 응용편 6

Go errors
Go errors

Go 언어를 처음 접하며 기본 문법을 익혔다면, 이제 실무에서 가장 중요한 부분 중 하나인 에러 핸들링을 제대로 다룰 때가 왔습니다. 단순히 if err != nil로 에러를 확인하는 것을 넘어서, Go errors 패키지를 활용한 고급 에러 핸들링 기법을 알아보겠습니다.

기본 에러 처리의 한계

Go 언어에서 기본적인 에러 처리는 다음과 같습니다:

Go
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

하지만 실제 프로덕션 환경에서는 더 복잡한 에러 처리가 필요합니다. 에러의 원인을 추적하고, 특정 에러 타입에 따라 다른 처리를 해야 하는 경우가 많기 때문입니다.

Go errors 패키지의 핵심 기능

errors 패키지는 Go 1.13부터 크게 개선되었습니다. 가장 중요한 기능들을 살펴보겠습니다.

errors.Is와 errors.As 함수 활용

Go
package main

import (
    "errors"
    "fmt"
    "os"
)

var (
    ErrNotFound = errors.New("file not found")
    ErrPermission = errors.New("permission denied")
)

func readFile(filename string) error {
    if filename == "" {
        return fmt.Errorf("filename cannot be empty: %w", ErrNotFound)
    }
    
    if !hasPermission(filename) {
        return fmt.Errorf("access denied for %s: %w", filename, ErrPermission)
    }
    
    return nil
}

func hasPermission(filename string) bool {
    // 실제 권한 체크 로직
    return filename != "secret.txt"
}

func main() {
    err := readFile("secret.txt")
    
    if errors.Is(err, ErrPermission) {
        fmt.Println("권한 문제로 파일을 읽을 수 없습니다.")
    } else if errors.Is(err, ErrNotFound) {
        fmt.Println("파일을 찾을 수 없습니다.")
    }
}

위 코드에서 errors.Is 함수는 에러 체인을 따라가며 특정 에러가 포함되어 있는지 확인합니다. fmt.Errorf%w 동사를 사용하여 에러를 래핑하면, 원본 에러 정보를 유지하면서 추가 컨텍스트를 제공할 수 있습니다.

커스텀 에러 타입 정의

Go
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s': %s", e.Field, e.Message)
}

func validateUser(name, email string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "name cannot be empty",
        }
    }
    
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "invalid email format",
        }
    }
    
    return nil
}

func handleUserValidation() {
    err := validateUser("", "invalid-email")
    
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Printf("검증 오류: %s 필드에서 %s\n", 
            validationErr.Field, validationErr.Message)
    }
}

errors.As 함수는 에러 체인에서 특정 타입의 에러를 찾아 변환해줍니다. 이를 통해 구조화된 에러 정보에 접근할 수 있습니다.

실무에서의 Go errors 활용 패턴

필자는 go언어를 활용하여 웹 사이트를 구성한 경험이 있으며, 그 과정에서 체계적인 에러 핸들링의 중요성을 깨달았습니다. 특히 사용자 요청 처리 중 발생하는 다양한 에러 상황을 적절히 분류하고 처리하는 것이 안정적인 서비스 운영에 필수적이었습니다.

에러 분류 체계 구축

Go
type ErrorType int

const (
    ErrorTypeValidation ErrorType = iota
    ErrorTypeNotFound
    ErrorTypePermission
    ErrorTypeInternal
)

type AppError struct {
    Type    ErrorType
    Message string
    Err     error
}

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

func (e *AppError) Unwrap() error {
    return e.Err
}

func NewValidationError(message string, err error) *AppError {
    return &AppError{
        Type:    ErrorTypeValidation,
        Message: message,
        Err:     err,
    }
}

func NewNotFoundError(message string) *AppError {
    return &AppError{
        Type:    ErrorTypeNotFound,
        Message: message,
    }
}

이런 체계적인 에러 분류를 통해 클라이언트에게 적절한 HTTP 상태 코드와 메시지를 반환할 수 있습니다.

에러 체이닝과 컨텍스트 보존

Go
func processOrder(orderID string) error {
    user, err := getUserByOrderID(orderID)
    if err != nil {
        return fmt.Errorf("주문 처리 중 사용자 조회 실패 (주문ID: %s): %w", orderID, err)
    }
    
    if err := validateUserPermission(user); err != nil {
        return fmt.Errorf("사용자 권한 검증 실패 (사용자ID: %s): %w", user.ID, err)
    }
    
    if err := updateOrderStatus(orderID, "processing"); err != nil {
        return fmt.Errorf("주문 상태 업데이트 실패 (주문ID: %s): %w", orderID, err)
    }
    
    return nil
}

func getUserByOrderID(orderID string) (*User, error) {
    // 데이터베이스 조회 로직
    if orderID == "invalid" {
        return nil, NewNotFoundError("주문을 찾을 수 없습니다")
    }
    return &User{ID: "user123"}, nil
}

func validateUserPermission(user *User) error {
    if user.ID == "blocked" {
        return NewValidationError("차단된 사용자입니다", nil)
    }
    return nil
}

func updateOrderStatus(orderID, status string) error {
    // 상태 업데이트 로직
    return nil
}

type User struct {
    ID string
}

이 예제에서 각 단계별로 에러가 발생하면 컨텍스트 정보를 추가하여 래핑합니다. 이를 통해 에러 발생 지점과 원인을 쉽게 추적할 수 있습니다.

고급 에러 핸들링 기법

에러 그룹핑과 다중 에러 처리

Go
import (
    "golang.org/x/sync/errgroup"
    "context"
)

func processMultipleFiles(filenames []string) error {
    g, ctx := errgroup.WithContext(context.Background())
    
    for _, filename := range filenames {
        filename := filename // 클로저 변수 캡처
        g.Go(func() error {
            return processFile(ctx, filename)
        })
    }
    
    if err := g.Wait(); err != nil {
        return fmt.Errorf("파일 처리 중 오류 발생: %w", err)
    }
    
    return nil
}

func processFile(ctx context.Context, filename string) error {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        // 파일 처리 로직
        if filename == "error.txt" {
            return errors.New("처리할 수 없는 파일입니다")
        }
        return nil
    }
}

errgroup을 사용하면 여러 고루틴에서 발생하는 에러를 효과적으로 관리할 수 있습니다.

실제 프로젝트에서의 경험

필자는 추후에 gin 웹 프레임워크를 통해 고도화된 사이트를 만들었는데, 이때 go언어 errors 패키지의 진가를 실감했습니다. 특히 미들웨어에서 에러를 중앙 집중식으로 처리하는 패턴을 적용했습니다:

Go
func ErrorMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors.Last().Err
            
            var appErr *AppError
            if errors.As(err, &appErr) {
                switch appErr.Type {
                case ErrorTypeValidation:
                    c.JSON(400, gin.H{"error": appErr.Message})
                case ErrorTypeNotFound:
                    c.JSON(404, gin.H{"error": appErr.Message})
                case ErrorTypePermission:
                    c.JSON(403, gin.H{"error": appErr.Message})
                default:
                    c.JSON(500, gin.H{"error": "내부 서버 오류"})
                }
            } else {
                c.JSON(500, gin.H{"error": "알 수 없는 오류"})
            }
        }
    }
}

이러한 패턴을 통해 모든 핸들러에서 일관된 에러 응답을 제공할 수 있었고, 에러 로깅과 모니터링도 중앙에서 처리할 수 있었습니다.

에러 처리 모범 사례

  1. 에러 래핑 시 충분한 컨텍스트 제공: 에러 발생 위치와 관련 정보를 포함하여 디버깅을 용이하게 합니다.
  2. 커스텀 에러 타입 활용: 에러 분류를 통해 적절한 처리 로직을 구현합니다.
  3. 에러 체이닝 유지: fmt.Errorf%w 동사를 사용하여 원본 에러 정보를 보존합니다.
  4. 중앙 집중식 에러 처리: 미들웨어나 공통 함수를 통해 일관된 에러 처리를 구현합니다.

Go의 errors 패키지를 제대로 활용하면 견고하고 유지보수가 용이한 애플리케이션을 개발할 수 있습니다. 단순한 에러 확인을 넘어서 체계적인 에러 관리 시스템을 구축하는 것이 성숙한 Go 개발자로 성장하는 핵심입니다.