에러 래핑과 언래핑
Go 1.13부터 에러 래핑(wrapping) 기능이 표준 라이브러리에 추가되었습니다. %w 동사와 errors.Is, errors.As를 사용하면 에러 체인을 통해 원인 에러를 추적할 수 있습니다.
%w로 에러 래핑
fmt.Errorf에 %w를 사용하면 기존 에러를 감싸면서 컨텍스트 정보를 추가할 수 있습니다.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func readFromDB(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return fmt.Sprintf("data-%d", id), nil
}
func getUser(id int) (string, error) {
data, err := readFromDB(id)
if err != nil {
// %w로 래핑 — 컨텍스트 추가
return "", fmt.Errorf("getUser(id=%d): %w", id, err)
}
return data, nil
}
func handleRequest(id int) error {
_, err := getUser(id)
if err != nil {
// 한 번 더 래핑
return fmt.Errorf("handleRequest: %w", err)
}
return nil
}
func main() {
err := handleRequest(-1)
if err != nil {
// 전체 에러 메시지 (체인 포함)
fmt.Println("에러:", err)
// 에러: handleRequest: getUser(id=-1): not found
// 원래 에러 확인 — errors.Is는 체인 전체를 탐색
fmt.Println("ErrNotFound?", errors.Is(err, ErrNotFound)) // true
// errors.Unwrap으로 한 단계씩 벗기기
fmt.Println("한 단계 언래핑:", errors.Unwrap(err))
// 한 단계 언래핑: getUser(id=-1): not found
fmt.Println("두 단계 언래핑:", errors.Unwrap(errors.Unwrap(err)))
// 두 단계 언래핑: not found
}
}
errors.Is — 에러 체인 탐색
errors.Is는 에러 체인을 따라가며 대상 에러와 일치하는지 확인합니다.
package main
import (
"errors"
"fmt"
)
// 센티넬 에러 정의
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("timeout")
)
// 에러 체인 구성
func level3() error {
return ErrNotFound
}
func level2() error {
err := level3()
return fmt.Errorf("level2 failed: %w", err)
}
func level1() error {
err := level2()
return fmt.Errorf("level1 failed: %w", err)
}
// Is 메서드를 구현한 커스텀 에러 (고급)
type TimeoutError struct {
Code int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout (code=%d)", e.Code)
}
// Is 메서드: 같은 타입의 에러인지 비교
func (e *TimeoutError) Is(target error) bool {
t, ok := target.(*TimeoutError)
if !ok {
return false
}
// Code가 0이면 모든 TimeoutError와 일치 (와일드카드)
return t.Code == 0 || e.Code == t.Code
}
func main() {
// 기본 errors.Is
err := level1()
fmt.Println("에러:", err)
// 에러: level1 failed: level2 failed: not found
fmt.Println("Is ErrNotFound:", errors.Is(err, ErrNotFound)) // true
fmt.Println("Is ErrPermission:", errors.Is(err, ErrPermission)) // false
fmt.Println("Is ErrTimeout:", errors.Is(err, ErrTimeout)) // false
// 직접 비교 — 체인을 탐색하지 않음
fmt.Println("\n직접 비교:", err == ErrNotFound) // false! 래핑되어 있음
// Is 메서드 구현 예제
specificErr := &TimeoutError{Code: 503}
wrappedErr := fmt.Errorf("request failed: %w", specificErr)
// 코드 503 확인
fmt.Println("\n503 타임아웃?", errors.Is(wrappedErr, &TimeoutError{Code: 503})) // true
// 코드 0으로 모든 TimeoutError 확인
fmt.Println("모든 타임아웃?", errors.Is(wrappedErr, &TimeoutError{Code: 0})) // true
// 코드 404는 아님
fmt.Println("404 타임아웃?", errors.Is(wrappedErr, &TimeoutError{Code: 404})) // false
// 실전: HTTP 에러 처리 패턴
var ErrHTTP404 = errors.New("HTTP 404")
var ErrHTTP500 = errors.New("HTTP 500")
handleError := func(err error) {
switch {
case errors.Is(err, ErrHTTP404):
fmt.Println("리소스를 찾을 수 없음")
case errors.Is(err, ErrHTTP500):
fmt.Println("서버 내부 오류")
case errors.Is(err, ErrNotFound):
fmt.Println("데이터 없음")
default:
fmt.Println("알 수 없는 에러:", err)
}
}
handleError(fmt.Errorf("api: %w", ErrHTTP404))
handleError(fmt.Errorf("db: %w", ErrNotFound))
}
errors.As — 에러 타입 추출
errors.As는 에러 체인에서 특정 타입의 에러를 찾아 추출합니다.
package main
import (
"errors"
"fmt"
)
// 상세 정보가 담긴 커스텀 에러 타입들
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: field=%q msg=%q", e.Field, e.Message)
}
type DatabaseError struct {
Query string
Code int
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error: query=%q code=%d", e.Query, e.Code)
}
// 에러가 발생하는 함수들
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{Field: "age", Message: "must be between 0 and 150"}
}
return nil
}
func queryUser(id int) (string, error) {
if id == 0 {
return "", &DatabaseError{Query: "SELECT * FROM users WHERE id=0", Code: 1048}
}
return fmt.Sprintf("user-%d", id), nil
}
func processUser(id, age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf("processUser: %w", err)
}
if _, err := queryUser(id); err != nil {
return fmt.Errorf("processUser: %w", err)
}
return nil
}
func main() {
// errors.As — 타입별 에러 처리
err := processUser(1, -5) // 유효하지 않은 나이
if err != nil {
fmt.Println("에러:", err)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("유효성 검사 실패: 필드=%q, 메시지=%q\n",
valErr.Field, valErr.Message)
// 유효성 검사 실패: 필드="age", 메시지="must be between 0 and 150"
}
}
// DB 에러
err = processUser(0, 25) // 유효하지 않은 ID
if err != nil {
fmt.Println("에러:", err)
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf("DB 에러: 쿼리=%q, 코드=%d\n", dbErr.Query, dbErr.Code)
// DB 에러: 쿼리="SELECT * FROM users WHERE id=0", 코드=1048
}
}
// errors.Is vs errors.As 비교
fmt.Println("\n--- Is vs As 비교 ---")
wrappedErr := fmt.Errorf("outer: %w", &ValidationError{Field: "email", Message: "invalid format"})
// errors.Is: 동일성 비교 (값이 같아야 함)
fmt.Println("Is ValidationError{} ?:", errors.Is(wrappedErr, &ValidationError{})) // false (값이 다름)
// errors.As: 타입 호환성 확인 후 추출
var extracted *ValidationError
fmt.Println("As *ValidationError ?:", errors.As(wrappedErr, &extracted)) // true
if extracted != nil {
fmt.Printf("추출된 에러: 필드=%q\n", extracted.Field) // 추출된 에러: 필드="email"
}
}
에러 체인 완전 탐색
에러 체인을 직접 순회하는 패턴입니다.
package main
import (
"errors"
"fmt"
)
// 중첩 래핑 에러 체인 생성
func buildErrorChain() error {
base := errors.New("base error")
wrapped1 := fmt.Errorf("layer 1: %w", base)
wrapped2 := fmt.Errorf("layer 2: %w", wrapped1)
wrapped3 := fmt.Errorf("layer 3: %w", wrapped2)
return wrapped3
}
// 에러 체인을 슬라이스로 수집
func collectErrors(err error) []error {
var chain []error
for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}
return chain
}
// Join으로 여러 에러 합치기 (Go 1.20+)
func processAll(ids []int) error {
var errs []error
for _, id := range ids {
if id <= 0 {
errs = append(errs, fmt.Errorf("invalid id: %d", id))
}
}
if len(errs) > 0 {
return errors.Join(errs...) // Go 1.20+
}
return nil
}
func main() {
// 체인 수동 탐색
err := buildErrorChain()
chain := collectErrors(err)
fmt.Printf("에러 체인 깊이: %d\n", len(chain))
for i, e := range chain {
fmt.Printf(" [%d] %v\n", i, e)
}
// 에러 체인 깊이: 4
// [0] layer 3: layer 2: layer 1: base error
// [1] layer 2: layer 1: base error
// [2] layer 1: base error
// [3] base error
// errors.Join 사용 (Go 1.20+)
fmt.Println()
joinErr := processAll([]int{1, -1, 0, 2, -3})
if joinErr != nil {
fmt.Println("복합 에러:", joinErr)
// Join된 에러는 Unwrap()이 []error를 반환하는 인터페이스 구현
type unwrapper interface {
Unwrap() []error
}
if uw, ok := joinErr.(unwrapper); ok {
errs := uw.Unwrap()
fmt.Printf("총 %d개 에러:\n", len(errs))
for _, e := range errs {
fmt.Printf(" - %v\n", e)
}
}
}
// 실전: 에러 체인에서 근본 원인(root cause) 추출
rootCause := func(err error) error {
for {
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
return err
}
err = unwrapped
}
}
chainErr := buildErrorChain()
fmt.Println("\n근본 원인:", rootCause(chainErr)) // 근본 원인: base error
}
실전 예제: 레이어드 에러 처리
실제 애플리케이션에서 에러 래핑을 사용하는 패턴입니다.
package main
import (
"errors"
"fmt"
)
// 도메인 에러
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
)
// 레이어별 에러 컨텍스트 추가
type UserRepository struct{}
func (r *UserRepository) FindByID(id int) (map[string]any, error) {
if id <= 0 {
return nil, fmt.Errorf("repository.FindByID: %w", ErrInvalidInput)
}
if id > 100 {
return nil, fmt.Errorf("repository.FindByID(id=%d): %w", id, ErrUserNotFound)
}
return map[string]any{"id": id, "name": fmt.Sprintf("User%d", id), "role": "user"}, nil
}
type UserService struct {
repo *UserRepository
}
func (s *UserService) GetProfile(requesterID, targetID int) (map[string]any, error) {
if requesterID <= 0 {
return nil, fmt.Errorf("service.GetProfile: requester: %w", ErrUnauthorized)
}
user, err := s.repo.FindByID(targetID)
if err != nil {
return nil, fmt.Errorf("service.GetProfile(target=%d): %w", targetID, err)
}
return user, nil
}
type UserHandler struct {
svc *UserService
}
func (h *UserHandler) Handle(requesterID, targetID int) {
profile, err := h.svc.GetProfile(requesterID, targetID)
if err != nil {
fmt.Printf("핸들러 에러: %v\n", err)
// 에러 종류에 따른 HTTP 상태코드 결정
switch {
case errors.Is(err, ErrUnauthorized):
fmt.Println("→ HTTP 401 Unauthorized")
case errors.Is(err, ErrUserNotFound):
fmt.Println("→ HTTP 404 Not Found")
case errors.Is(err, ErrInvalidInput):
fmt.Println("→ HTTP 400 Bad Request")
default:
fmt.Println("→ HTTP 500 Internal Server Error")
}
return
}
fmt.Printf("프로필 조회 성공: %v\n", profile)
}
func main() {
repo := &UserRepository{}
svc := &UserService{repo: repo}
handler := &UserHandler{svc: svc}
fmt.Println("=== 정상 조회 ===")
handler.Handle(1, 42)
fmt.Println("\n=== 인증 실패 ===")
handler.Handle(0, 42)
fmt.Println("\n=== 사용자 없음 ===")
handler.Handle(1, 999)
fmt.Println("\n=== 잘못된 입력 ===")
handler.Handle(1, -1)
}
핵심 정리
fmt.Errorf("msg: %w", err)— 에러를 래핑하면서 컨텍스트 추가errors.Is(err, target)— 체인 전체에서 특정 에러 값 탐색errors.As(err, &target)— 체인에서 특정 타입 에러 추출errors.Unwrap(err)— 한 단계 언래핑errors.Join(errs...)— 여러 에러를 하나로 합치기 (Go 1.20+)- 에러 래핑은 컨텍스트를 추가하되 근본 원인을 보존한다