본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 5장: 에러 처리 패턴과 베스트 프랙티스
2026년 8월 18일·프로그래밍·

5장: 에러 처리 패턴과 베스트 프랙티스

Go의 에러 값 철학, errors.Is/As를 활용한 에러 검사, fmt.Errorf와 %w를 사용한 에러 래핑, 커스텀 에러 타입, 센티널 에러, panic/recover의 올바른 사용법을 다룹니다.

15분689자10개 섹션
concurrencyperformancedesign-patternstestinginfrastructure
공유
go-backend5 / 11
1234567891011
이전4장: 동시성 패턴 심화 - select, context, errgroup다음6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo

학습 목표

  • Go의 에러 값(Error Values) 철학을 이해한다
  • errors.Is와 errors.As의 차이와 사용법을 익힌다
  • 에러 래핑(Error Wrapping)과 컨텍스트 추가를 적용할 수 있다
  • 커스텀 에러 타입과 센티널 에러를 올바르게 설계할 수 있다
  • panic과 recover의 적절한 사용 범위를 파악한다

Go의 에러 철학

Go에서 에러는 특별한 제어 흐름이 아닌 값입니다. error는 단 하나의 메서드를 가진 인터페이스입니다.

error-interface.go
go
type error interface {
    Error() string
}

이 단순한 인터페이스가 Go 에러 처리의 모든 기반입니다. 예외(Exception) 기반 언어에서는 에러가 호출 스택을 따라 암묵적으로 전파되지만, Go에서는 모든 에러를 명시적으로 확인하고 처리해야 합니다.

explicit-error-check.go
go
result, err := doSomething()
if err != nil {
    return fmt.Errorf("작업 실패: %w", err)
}
// result 사용
Info

if err != nil 패턴이 반복적으로 보인다고 해서 Go의 에러 처리가 부족한 것은 아닙니다. 이 명시성 덕분에 코드를 읽을 때 에러 처리 경로가 항상 눈에 보이며, 에러를 무시하는 실수를 방지할 수 있습니다.


에러 생성

errors.New와 fmt.Errorf

가장 기본적인 에러 생성 방법입니다.

create-errors.go
go
import (
    "errors"
    "fmt"
)
 
// 단순 에러
err := errors.New("사용자를 찾을 수 없습니다")
 
// 포맷이 포함된 에러
err := fmt.Errorf("사용자 ID %d를 찾을 수 없습니다", userID)
 
// 에러 래핑 -- %w 동사 사용
err := fmt.Errorf("사용자 조회 실패: %w", originalErr)

센티널 에러

센티널 에러(Sentinel Error)는 패키지 수준에서 미리 정의된 에러 값입니다. 호출자가 특정 에러 조건을 검사할 수 있게 합니다.

sentinel-errors.go
go
package repository
 
import "errors"
 
var (
    ErrNotFound      = errors.New("리소스를 찾을 수 없습니다")
    ErrAlreadyExists = errors.New("리소스가 이미 존재합니다")
    ErrUnauthorized  = errors.New("인증이 필요합니다")
    ErrForbidden     = errors.New("접근 권한이 없습니다")
)

표준 라이브러리에서도 센티널 에러를 널리 사용합니다. io.EOF, sql.ErrNoRows, context.Canceled, context.DeadlineExceeded 등이 대표적입니다.

Warning

센티널 에러는 패키지의 공개 API의 일부가 됩니다. 한번 공개하면 하위 호환성을 유지해야 하므로, 정말 필요한 경우에만 정의하세요.


에러 래핑과 언래핑

fmt.Errorf와 %w

Go 1.13에서 도입된 %w 동사를 사용하면 원본 에러를 래핑하면서 컨텍스트를 추가할 수 있습니다.

error-wrapping.go
go
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        // 원본 에러를 래핑하여 컨텍스트 추가
        return nil, fmt.Errorf("UserService.GetUser(id=%d): %w", id, err)
    }
    return user, nil
}

래핑된 에러는 체인을 형성합니다.

에러 체인 예시
text
UserService.GetUser(id=42): UserRepo.FindByID: sql: no rows in result set

errors.Is -- 에러 비교

errors.Is는 에러 체인을 탐색하면서 특정 에러와 일치하는지 확인합니다.

errors-is.go
go
user, err := userService.GetUser(ctx, 42)
if err != nil {
    if errors.Is(err, repository.ErrNotFound) {
        // 404 Not Found 응답
        http.Error(w, "사용자를 찾을 수 없습니다", http.StatusNotFound)
        return
    }
    if errors.Is(err, context.DeadlineExceeded) {
        // 504 Gateway Timeout 응답
        http.Error(w, "요청 시간 초과", http.StatusGatewayTimeout)
        return
    }
    // 500 Internal Server Error
    http.Error(w, "서버 내부 오류", http.StatusInternalServerError)
}

errors.Is는 직접 비교(==)와 달리 에러 체인 전체를 탐색합니다.

errors.As -- 타입 단언

errors.As는 에러 체인에서 특정 타입의 에러를 찾아 추출합니다.

errors-as.go
go
var validationErr *ValidationError
if errors.As(err, &validationErr) {
    // ValidationError의 필드에 접근 가능
    fmt.Println("유효하지 않은 필드:", validationErr.Field)
    fmt.Println("에러 메시지:", validationErr.Message)
}

커스텀 에러 타입

구조화된 에러 정보가 필요할 때 커스텀 에러 타입을 정의합니다.

기본 커스텀 에러

custom-error.go
go
type ValidationError struct {
    Field   string
    Message string
    Value   any
}
 
func (e *ValidationError) Error() string {
    return fmt.Sprintf("유효성 검사 실패 - 필드: %s, 메시지: %s", e.Field, e.Message)
}
 
func validateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "올바른 이메일 형식이 아닙니다",
            Value:   email,
        }
    }
    return nil
}

복합 에러 타입

HTTP API에서 사용할 수 있는 구조화된 에러 타입입니다.

api-error.go
go
type APIError struct {
    Code       int    `json:"code"`
    Message    string `json:"message"`
    Detail     string `json:"detail,omitempty"`
    RequestID  string `json:"request_id,omitempty"`
    Err        error  `json:"-"` // JSON 직렬화에서 제외
}
 
func (e *APIError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
 
func (e *APIError) Unwrap() error {
    return e.Err
}
 
// 생성 헬퍼 함수
func NewNotFoundError(resource string, id any) *APIError {
    return &APIError{
        Code:    404,
        Message: fmt.Sprintf("%s을(를) 찾을 수 없습니다", resource),
        Detail:  fmt.Sprintf("ID: %v", id),
    }
}
 
func NewInternalError(err error) *APIError {
    return &APIError{
        Code:    500,
        Message: "서버 내부 오류가 발생했습니다",
        Err:     err,
    }
}
Tip

Unwrap() error 메서드를 구현하면 errors.Is와 errors.As가 에러 체인을 탐색할 수 있습니다. 커스텀 에러가 다른 에러를 감싸고 있다면 반드시 Unwrap을 구현하세요.


에러 처리 패턴

패턴 1: 조기 반환

에러가 발생하면 즉시 반환하여 정상 경로를 왼쪽 정렬로 유지합니다.

early-return.go
go
func processOrder(ctx context.Context, orderID int64) error {
    order, err := findOrder(ctx, orderID)
    if err != nil {
        return fmt.Errorf("주문 조회 실패: %w", err)
    }
 
    if err := validateOrder(order); err != nil {
        return fmt.Errorf("주문 유효성 검사 실패: %w", err)
    }
 
    if err := chargePayment(ctx, order); err != nil {
        return fmt.Errorf("결제 처리 실패: %w", err)
    }
 
    if err := shipOrder(ctx, order); err != nil {
        return fmt.Errorf("배송 처리 실패: %w", err)
    }
 
    return nil
}

패턴 2: 에러에 컨텍스트 추가

에러를 래핑할 때는 어디서, 무엇을 하다가 실패했는지 정보를 추가합니다.

error-context.go
go
// 나쁜 예 -- 컨텍스트 없음
return err
 
// 나쁜 예 -- 중복 정보
return fmt.Errorf("에러 발생: %w", err)
 
// 좋은 예 -- 함수명과 핵심 파라미터 포함
return fmt.Errorf("UserRepo.FindByEmail(%s): %w", email, err)

패턴 3: 에러 처리 위치 결정

에러를 처리할 것인지 전파할 것인지는 명확한 기준이 필요합니다.

error-handling-decision.go
go
// 하위 계층 -- 에러를 래핑하여 전파
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
    var user User
    err := r.db.QueryRowContext(ctx, query, id).Scan(&user.ID, &user.Name)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrNotFound // 도메인 에러로 변환
        }
        return nil, fmt.Errorf("UserRepo.FindByID(%d): %w", id, err)
    }
    return &user, nil
}
 
// 상위 계층 -- 에러를 처리하고 사용자에게 응답
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(r.Context(), userID)
    if err != nil {
        if errors.Is(err, repository.ErrNotFound) {
            writeJSON(w, http.StatusNotFound, ErrorResponse{
                Message: "사용자를 찾을 수 없습니다",
            })
            return
        }
        slog.Error("사용자 조회 실패", "error", err, "userID", userID)
        writeJSON(w, http.StatusInternalServerError, ErrorResponse{
            Message: "서버 내부 오류",
        })
        return
    }
    writeJSON(w, http.StatusOK, user)
}

panic과 recover

panic의 올바른 사용

panic은 프로그램이 더 이상 안전하게 실행될 수 없는 상황에서만 사용합니다.

panic-usage.go
go
// 적절한 panic 사용 사례
func MustParseURL(rawURL string) *url.URL {
    u, err := url.Parse(rawURL)
    if err != nil {
        panic(fmt.Sprintf("잘못된 URL: %s: %v", rawURL, err))
    }
    return u
}
 
// 초기화 시 필수 환경 변수 확인
func MustGetEnv(key string) string {
    val := os.Getenv(key)
    if val == "" {
        panic(fmt.Sprintf("필수 환경 변수 %s가 설정되지 않았습니다", key))
    }
    return val
}
Danger

일반적인 비즈니스 로직에서 panic을 사용하지 마세요. panic은 프로그래밍 에러(nil 포인터 역참조, 범위 초과 접근)나 초기화 실패 같은 복구 불가능한 상황에서만 사용해야 합니다. 네트워크 에러, 유효성 검사 실패, 데이터 부재 등은 모두 error 값으로 처리합니다.

recover를 활용한 복구

recover는 panic에서 복구하여 프로그램이 계속 실행되도록 합니다. HTTP 서버에서 하나의 요청 처리 중 panic이 발생해도 서버 전체가 죽지 않도록 하는 데 사용됩니다.

recover-middleware.go
go
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // 스택 트레이스 로깅
                stack := debug.Stack()
                slog.Error("panic 복구",
                    "panic", rec,
                    "stack", string(stack),
                    "method", r.Method,
                    "path", r.URL.Path,
                )
 
                http.Error(w, "서버 내부 오류", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

에러 처리 베스트 프랙티스

실전에서 적용할 수 있는 에러 처리 원칙들을 정리합니다.

에러 처리 체크리스트
text
1. 에러를 무시하지 않는다 (명시적으로 _ 할당은 가능)
2. 에러를 래핑할 때 컨텍스트(함수명, 파라미터)를 추가한다
3. 센티널 에러 비교는 errors.Is, 타입 검사는 errors.As를 사용한다
4. 에러 메시지는 소문자로 시작하고 마침표로 끝나지 않는다 (Go 관례)
5. panic은 복구 불가능한 상황에서만 사용한다
6. 에러를 로깅하고 반환하는 것을 동시에 하지 않는다 (중복 로깅 방지)
7. 라이브러리는 에러를 반환하고, 애플리케이션이 에러를 로깅한다
Tip

"에러를 처리하거나 전파하라, 둘 다 하지 마라"가 핵심 원칙입니다. 에러를 로깅하고 동시에 반환하면 상위 호출자에서 또 로깅하게 되어 같은 에러가 여러 번 기록됩니다.


정리

이번 장에서 살펴본 핵심 내용을 정리합니다.

  • Go에서 에러는 값이며, error 인터페이스를 통해 명시적으로 전달되고 처리됩니다
  • %w 동사로 에러를 래핑하고, errors.Is로 센티널 에러를, errors.As로 타입을 검사합니다
  • 센티널 에러는 패키지의 공개 API로, 커스텀 에러 타입은 구조화된 정보가 필요할 때 사용합니다
  • panic은 복구 불가능한 상황에서만, recover는 HTTP 서버 같은 장기 실행 프로세스의 안정성을 위해 사용합니다
  • 에러를 "처리하거나 전파하라, 둘 다 하지 마라"가 핵심 원칙입니다

다음 장 미리보기

6장에서는 Go 웹 프레임워크 비교를 다룹니다. 표준 라이브러리 net/http부터 Gin, Chi, Fiber, Echo까지, 각 프레임워크의 아키텍처와 특성을 비교하고 프로젝트 유형에 따른 선택 기준을 제시합니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo

Go의 표준 라이브러리 net/http부터 Gin, Chi, Fiber, Echo까지 주요 웹 프레임워크의 아키텍처, 성능 특성, 미들웨어 구조를 비교하고 프로젝트별 선택 기준을 제시합니다.

2026년 8월 20일·12분
프로그래밍

4장: 동시성 패턴 심화 - select, context, errgroup

Go의 고급 동시성 패턴인 select 문, context.Context를 활용한 취소/타임아웃, errgroup, 팬아웃/팬인, 파이프라인, 워커 풀, 레이트 리미팅 패턴을 다룹니다.

2026년 8월 15일·14분
프로그래밍

7장: 데이터베이스 연동 - SQL, ORM, 마이그레이션

Go에서 데이터베이스를 다루는 방법을 database/sql, sqlx, GORM, pgx를 통해 비교하고, 커넥션 풀링, 트랜잭션, 마이그레이션, 쿼리 빌더 패턴을 실전 예제로 다룹니다.

2026년 8월 23일·14분
이전 글4장: 동시성 패턴 심화 - select, context, errgroup
다음 글6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo

댓글

목차

약 15분 남음
  • 학습 목표
  • Go의 에러 철학
  • 에러 생성
    • errors.New와 fmt.Errorf
    • 센티널 에러
  • 에러 래핑과 언래핑
    • fmt.Errorf와 %w
    • errors.Is -- 에러 비교
    • errors.As -- 타입 단언
  • 커스텀 에러 타입
    • 기본 커스텀 에러
    • 복합 에러 타입
  • 에러 처리 패턴
    • 패턴 1: 조기 반환
    • 패턴 2: 에러에 컨텍스트 추가
    • 패턴 3: 에러 처리 위치 결정
  • panic과 recover
    • panic의 올바른 사용
    • recover를 활용한 복구
  • 에러 처리 베스트 프랙티스
  • 정리
  • 다음 장 미리보기