Go의 에러 핸들링 전략과 패닉 복구 기법

error 인터페이스의 동작 원리

Go에서 에러는 error 인터페이스로 추상화됩니다. 이 인터페이스는 단일 메서드만을 요구하며, 이는 Go의 에러 처리가 본질적으로 단순하면서도 강력한 이유입니다.

type error interface {
    Error() string
}

이 설계의 핵심은 모든 타입이 에러가 될 수 있다는 점입니다. 문자열 뿐만 아니라 복잡한 구조체도 Error() 메서드를 구현함으로써 에러로 활용 가능합니다.

맞춤형 에러 구조체 설계

단순한 문자열 메시지를 넘어, 에러에 메타데이터를 부여하면 디버깅과 모니터링이 크게 향상됩니다. 다음은 HTTP 상태 코드와 연동된 에러 타입의 예시입니다:

import "fmt"

type HTTPFault struct {
    Status  int
    Detail  string
    TraceID string
}

func (hf *HTTPFault) Error() string {
    return fmt.Sprintf("[Trace: %s] HTTP %d - %s", hf.TraceID, hf.Status, hf.Detail)
}

에러 생성의 세 가지 방식

1. errors.New - 정적 에러

import "errors"

var ErrQuotaExceeded = errors.New("rate limit exceeded")

2. fmt.Errorf - 동적 포맷팅

path := "/tmp/data.txt"
err := fmt.Errorf("cannot access resource: %s", path)

3. 에러 체인 구성 (Go 1.13+)

%w 동사를 활용하면 에러를 계층적으로 포장할 수 있습니다. 이는 원인 추적(cause tracking)에 필수적입니다.

func fetchData(endpoint string) error {
    if err := connect(endpoint); err != nil {
        return fmt.Errorf("fetchData failed for endpoint %s: %w", endpoint, err)
    }
    return nil
}

에러 탐색: Is와 As의 차이

함수용도비교 방식
errors.Is동일한 에러 인스턴스 식별== 비교 및 Unwrap 재귀
errors.As특정 타입으로 추출타입 단언(assertion)
// 특정 에러가 발생했는지 확인
if errors.Is(err, ErrQuotaExceeded) {
    return http.StatusTooManyRequests
}

// 커스텀 타입으로 추출
var httpErr *HTTPFault
if errors.As(err, &httpErr) {
    log.Printf("Request trace: %s", httpErr.TraceID)
}

panic과 recover의 실행 락

panic은 일반적인 에러와 달리 현재 고루틴의 실행을 즉시 중단시킵니다. 하지만 defer로 등록된 함수들은 LIFO 순서로 반드시 실행됩니다.

func fragileOperation() {
    defer fmt.Println("cleanup: resource A")
    defer fmt.Println("cleanup: resource B")
    
    panic("critical failure")  // 이 시점에서 위로 역순 실행
    
    // 이 코드는 도달하지 않음
}

출력:

cleanup: resource B
cleanup: resource A
panic: critical failure

안전한 복구 패턴

recover의 함정은 직접 호출한 함수에서만 작동한다는 점입니다. 다음은 견고한 복구 구조입니다:

func WithRecovery(task func()) {
    defer func() {
        if cause := recover(); cause != nil {
            log.Printf("Recovered from: %v\nStack: %s", 
                cause, debug.Stack())
        }
    }()
    
    task()  // 실제 작업 위임
}

에러 반환 vs 패닉: 의사결정 프레임워크

에러 반환이 적합한 경우:

  • 입력값 검증 실패 (잘못된 파라미터)
  • 외부 시스템 일시적 장애 (네트워크 타임아웃)
  • 권한 부족

패닉이 적합한 경우:

  • 프로그램 불변조건(invariant) 위반
  • 초기화 단계의 치명적 설정 오류
  • nil 포인터 역참조 방지를 위한 방어적 프로그래밍
// 에러 반환 예시: 클라이언트가 대응 가능한 상황
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero: a=%.2f", a)
    }
    return a / b, nil
}

// 패닉 예시: 프로그램 버그로 간주해야 하는 상황
func MustParseConfig(raw []byte) Config {
    cfg, err := parse(raw)
    if err != nil {
        panic(fmt.Sprintf("invalid config: %v", err))
    }
    return cfg
}

태그: go error-handling panic-recover errors-package defer-pattern

7월 9일 00:25에 게시됨