Go에서 wrapping errors.Is 및 errors.As를 사용하여 오류 처리 강화
Grace Collins
Solutions Engineer · Leapcell

Go 오류 해독으로 진단 개선
오류 처리는 강력한 소프트웨어 작성의 필수적인 부분이며, Go는 명시적인 오류 반환 값을 통해 이를 강조합니다.
오랫동안 Go 개발자들은 오류를 검사하고 구별하기 위해 문자열 일치 또는 타입 단언에 의존하는 경우가 많았으며, 이는 불안정하고 유지 관리가 어려운 코드로 이어졌습니다.
Go 1.13에서 오류 래핑의 도입은 errors.Is
및 errors.As
함수와 결합되어 이러한 환경을 혁신했습니다. 이 현대적인 접근 방식은 Go의 단순성을 희생하지 않으면서 더 풍부한 컨텍스트와 더 정확한 의사 결정을 가능하게 하는 훨씬 더 강력하고 관용적인 오류 처리 방법을 제공합니다.
이 글에서는 오류 래핑, errors.Is
, errors.As
의 현대적인 사용법을 자세히 살펴보고, 개발자가 더 복원력 있고 관찰 가능한 Go 애플리케이션을 구축하는 데 어떻게 도움이 되는지 보여줍니다.
오류 래핑 및 검사 세분화
실제 예제를 살펴보기 전에 현대 Go 오류 처리와 관련된 핵심 개념을 명확히 이해해 봅시다.
-
오류 래핑: 이 메커니즘을 통해 오류는 다른, 하위 오류를 포함할 수 있습니다. 이는 원본 오류에 컨텍스트 계층을 추가하는 것과 같습니다. 이를 통해 오류 계보를 명확하게 추적하여 근본 원인까지 추적하고 중간 세부 정보를 노출할 수 있습니다. Go에서 오류 래핑은 일반적으로
fmt.Errorf
의%w
동사를 사용하여 달성됩니다. -
errors.Is
: 이 함수는 오류 체인을 재귀적으로 풀고 체인의 오류가 대상 오류와 "일치"하는지 확인합니다. 이것은 호출 스택 어디에서 발생했는지에 관계없이 특정 종류의 오류가 발생했는지 확인하기 위해 설계되었습니다. 미리 정의된 센티널 오류에 대해 확인하는 데 특히 유용합니다. -
errors.As
:errors.Is
와 유사하게errors.As
도 오류 체인을 풉니다. 그러나 동등성을 확인하는 대신 대상 타입으로 할당될 수 있는 체인에서 오류를 찾으려고 시도합니다. 일치하는 항목을 찾으면 하위 오류가 대상 변수에 할당됩니다. 번거로운 문자열 구문에 의존하지 않고 사용자 정의 오류 타입에서 특정 정보 또는 동작을 추출해야 할 때 매우 중요합니다.
코드 예제를 통해 이러한 개념을 설명해 보겠습니다.
기본 오류 래핑
파일 작업이 실패하는 시나리오를 생각해 봅시다. 원본 OS 오류를 더 많은 컨텍스트로 래핑할 수 있습니다.
package main import ( "errors" "fmt" "os" ) func readFile(filename string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { // 원본 오류를 추가 컨텍스트로 래핑 return nil, fmt.Errorf("failed to read file '%s': %w", filename, err) } return data, nil } func main() { _, err := readFile("nonexistent.txt") if err != nil { fmt.Println("Error:", err) // Output: Error: failed to read file 'nonexistent.txt': open nonexistent.txt: no such file or directory } }
이 예제에서 fmt.Errorf("failed to read file '%s': %w", filename, err)
은 os.ReadFile
오류를 래핑합니다.
%w
동사는 fmt.Errorf
에 errors.Unwrap
(및 결과적으로 errors.Is
및 errors.As
)에서 err
를 검색 가능하도록 지시합니다.
센티널 오류에 errors.Is
사용
우리는 특정 조건을 의미하기 위해 종종 센티널 오류를 정의합니다.
errors.Is
를 사용하면 이러한 조건을 쉽게 확인할 수 있습니다.
package main import ( "errors" "fmt" "os" ) var ErrRecordNotFound = errors.New("record not found") func getUser(id int) (string, error) { if id < 1 { return "", fmt.Errorf("invalid user ID: %d: %w", id, ErrRecordNotFound) } if id == 123 { return "John Doe", nil } return "", fmt.Errorf("user with ID %d not found: %w", id, ErrRecordNotFound) } func main() { _, err := getUser(0) if errors.Is(err, ErrRecordNotFound) { fmt.Println("User not found or invalid ID:", err) // Output: User not found or invalid ID: invalid user ID: 0: record not found } _, err = getUser(456) if errors.Is(err, ErrRecordNotFound) { fmt.Println("User not found or invalid ID:", err) // Output: User not found or invalid ID: user with ID 456 not found: record not found } _, err = getUser(123) if err != nil { fmt.Println("This should not happen:", err) } }
여기서 getUser
는 다양한 시나리오에서 ErrRecordNotFound
를 래핑합니다.
main
에서 errors.Is(err, ErrRecordNotFound)
는 반환된 오류 문자열이 다르더라도 기본 원인이 ErrRecordNotFound
일 때 올바르게 식별합니다.
사용자 정의 오류 타입을 위한 errors.As
활용
사용자 정의 오류 타입에서 특정 데이터나 메서드 동작을 추출해야 할 때 errors.As
가 사용할 도구입니다.
package main import ( "errors" "fmt" "time" ) // PermissionError는 누락된 권한에 대한 세부 정보를 포함하는 사용자 정의 오류 타입입니다. type PermissionError struct { User string Action string Missing string When time.Time } func (e *PermissionError) Error() string { return fmt.Sprintf("user %s cannot %s, missing permission '%s' at %v", e.User, e.Action, e.Missing, e.When) } func checkPermission(user, action string) error { if user == "guest" && action == "delete" { return &PermissionError{ User: user, Action: action, Missing: "delete_access", When: time.Now(), } } return nil } func performAction(user, action string) error { err := checkPermission(user, action) if err != nil { // 작업 시도에 대한 더 많은 컨텍스트로 권한 오류를 래핑 return fmt.Errorf("failed to perform action '%s' for user '%s': %w", action, user, err) } fmt.Printf("User %s successfully performed action %s.\n", user, action) return nil } func main() { err := performAction("guest", "delete") if err != nil { fmt.Println("Encountered error:", err) var pErr *PermissionError if errors.As(err, &pErr) { fmt.Printf("A permission error occurred! User: %s, Action: %s, Missing Permission: %s\n", pErr.User, pErr.Action, pErr.Missing) // Output: User: guest, Action: delete, Missing Permission: delete_access } else { fmt.Println("It was a different type of error.") } } err = performAction("admin", "delete") if err != nil { fmt.Println("This should not happen:", err) } // Output: User admin successfully performed action delete. }
이 예제에서는 performAction
이 PermissionError
를 래핑합니다.
main
에서 errors.As(err, &pErr)
는 래핑된 오류 체인에서 *PermissionError
를 성공적으로 추출하여 해당 필드(User
, Action
, Missing
)에 액세스할 수 있도록 합니다.
이는 문자열 구문에 의존하는 것보다 특정 오류 조건을 처리하고 프로그래밍 방식으로 반응하는 강력한 방법을 제공합니다.
언제 래핑하고 언제 래핑하지 않는가
오류 래핑은 원본 원인을 유지하면서 오류에 컨텍스트 정보를 추가해야 할 때 빛을 발합니다. 이는 디버깅 및 로깅에 매우 중요합니다. 하지만 만능은 아닙니다. 다음과 같은 경우에는 오류 래핑을 피하십시오.
- 오류가 순전히 일시적이거나 재시도 가능할 때: 호출 코드가 재시도해야 하는지 여부만 알면 되는 경우 간단하고 래핑되지 않은 오류 또는 부울 반환으로 충분할 수 있습니다.
- 오류가 내부적이며 전파되어서는 안 될 때: 오류가 내부 구현 세부 사항이고 상위 계층에 노출하면 캡슐화를 위반하는 경우 더 일반적인 외부 오류로 변환하여 반환하거나 단순히 로깅하고 미리 정의된 오류를 반환하는 것을 고려하십시오.
- 애플리케이션의 맨 위 수준에 있을 때: 맨 위에서는 전체 오류 체인을 로깅하고 종료하거나 일반적인 사용자 친화적인 메시지를 반환할 수 있습니다.
일반적인 원칙은 추가 컨텍스트의 형태로 가치를 더할 때 오류를 래핑하고, 다운스트림 소비자가 특정 오류 조건에 반응해야 하는 경우 검사할 수 있도록 하는 것입니다.
통합 오류 전략의 힘
올바른 오류 래핑을 통한 errors.Is
및 errors.As
를 중심으로 하는 현대 Go 오류 처리 접근 방식은 더 유지 관리 가능하고 복원력 있는 코드베이스를 육성합니다.
이는 불안정하고 깨지기 쉬운 문자열 비교 및 타입 단언에서 벗어나 오류를 식별하고 반응하는 구조화되고 의미론적인 방식으로 나아갑니다.
명확한 오류 계보를 제공하고 정확한 검사를 가능하게 함으로써 이러한 도구는 복잡한 애플리케이션에서 문제의 진단 가능성을 크게 향상시켜 궁극적으로 더 강력하고 안정적인 소프트웨어를 제공합니다.
요약하자면, errors.Is
및 errors.As
를 사용한 Go의 오류 래핑은 오류 컨텍스트를 풍부하게 하고 오류 체인을 정확하게 검사할 수 있는 강력하고 관용적인 방법을 제공합니다.
이는 더 강력하고 유지 관리 가능하며 디버깅 가능한 Go 애플리케이션으로 이어집니다.