Go에서 오류를 처리하는 방식 알아보기
프로그램을 작성할 때는 프로그램이 실패할 수 있는 다양한 방식을 생각해 보고 실패를 관리해야 합니다. 사용자에게 길고 복잡해 보이는 스택 추적 오류가 표시되면 안 되겠죠. 그 대신 어떤 문제가 발생했는지를 알려 주는 의미 있는 정보가 표시되어야 합니다. Go에는 panic
과 recover
같이 프로그램에서 예외(예기치 않은 동작)를 관리하기 위한 기본 제공 함수가 있습니다. 하지만 오류란 프로그램이 기본적으로 처리해야 하는 알려진 실패입니다.
Go의 오류 처리 접근 방식은 if
문과 return
문만 필요한 단순한 제어 흐름 메커니즘입니다. 예를 들어, employee
개체로부터 정보를 가져오기 위해 함수를 호출할 때는 해당 직원이 실제로 존재하는지 확인하는 것이 좋겠죠. Go는 다음과 같이 직설적인 방식으로 이러한 예상되는 오류를 처리합니다.
employee, err := getInformation(1000)
if err != nil {
// Something is wrong. Do something.
}
getInformation
함수가 employee
구조체를 반환하고 두 번째 값으로 오류를 반환하는 것을 알 수 있습니다. 오류가 nil
일 수 있습니다. 오류가 nil
이면 성공한 것입니다. 오류가 nil
이 아니면 실패한 것입니다. nil
이 아닌 오류가 발생하면 출력하거나 더 좋게는 로깅할 수 있는 오류 메시지가 발생합니다. Go에서는 이와 같은 방식으로 오류를 처리합니다. 다음 섹션에서 몇 가지 전략을 더 살펴보겠습니다.
Go의 오류 처리 방식에서는 오류를 보고하고 처리하는 방식에 주의를 기울여야 한다는 사실을 알아채셨을 것입니다. 정확히 보셨습니다. 이번에는 Go의 오류 처리 접근 방식을 잘 보여 주는 몇 가지 예제를 더 살펴보겠습니다.
구조체에 사용한 코드 조각을 사용하여 다양한 오류 처리 전략을 연습해 보겠습니다.
package main
import (
"fmt"
"os"
)
type Employee struct {
ID int
FirstName string
LastName string
Address string
}
func main() {
employee, err := getInformation(1001)
if err != nil {
// Something is wrong. Do something.
} else {
fmt.Print(employee)
}
}
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
return employee, err
}
func apiCallEmployee(id int) (*Employee, error) {
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}
여기서부터는 getInformation
, apiCallEmployee
, main
함수를 수정하여 오류를 처리하는 방법을 설명하겠습니다.
오류 처리 전략
함수가 오류를 반환하는 경우, 오류는 보통 마지막 반환 값이 됩니다. 앞 섹션에서 살펴본 것처럼 호출자는 오류가 존재하는지 검사하고 처리해야 합니다. 따라서 이 패턴에 따라 하위 루틴으로 오류를 전파하는 것이 자주 사용되는 전략 중 하나입니다. 예를 들어, 앞에 나온 예제의 getInformation
과 같은 하위 루틴은 다음과 같이 다른 조치는 취하지 않고 호출자에게 오류를 반환할 수 있습니다.
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, err // Simply return the error to the caller.
}
return employee, nil
}
오류를 전파하기 전에 추가 정보를 포함할 수도 있습니다. 이를 위해서는 fmt.Errorf()
함수를 사용합니다. 이 함수는 앞에서 본 것과 비슷하지만 오류를 반환한다는 점이 다릅니다. 예를 들어, 다음과 같이 오류에 컨텍스트를 추가하면서도 원래 오류를 반환할 수 있습니다.
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
}
return employee, nil
}
또 다른 전략은 오류가 일시적인 것일 경우 재시도 논리를 실행하는 것입니다. 예를 들어, 다음과 같이 재시도 정책을 사용하여 함수를 세 번 호출하고 2초 동안 기다릴 수 있습니다.
func getInformation(id int) (*Employee, error) {
for tries := 0; tries < 3; tries++ {
employee, err := apiCallEmployee(1000)
if err == nil {
return employee, nil
}
fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
}
return nil, fmt.Errorf("server has failed to respond to get the employee information")
}
마지막으로, 오류를 콘솔에 출력하는 대신 로깅하고 최종 사용자로부터 구현 세부 사항을 숨기는 전략이 있습니다. 로깅에 대해서는 다음 모듈에서 살펴보겠습니다. 여기서는 사용자 지정 오류를 만들고 사용하는 방법을 알아보겠습니다.
재사용 가능 오류 만들기
오류 메시지의 수가 점점 늘어나면 질서를 유지해야 할 수 있습니다. 또는 자주 사용하는 오류 메시지를 재사용할 수 있도록 라이브러리를 만들어야 할 수 있습니다. Go에서는 다음과 같이 errors.New()
함수를 사용하여 오류를 만들고 다양한 곳에서 재사용할 수 있습니다.
var ErrNotFound = errors.New("Employee not found!")
func getInformation(id int) (*Employee, error) {
if id != 1001 {
return nil, ErrNotFound
}
employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}
getInformation
함수가 더 깔끔해졌습니다. 게다가 오류 메시지를 변경해야 할 경우 이제 한 곳에서 하면 됩니다. 오류 변수에 Err
접두사를 포함하는 것이 규칙인 것도 알 수 있습니다.
마지막으로, 오류 변수를 사용하면 호출자 함수에서 오류를 더 구체적으로 처리할 수 있습니다. errors.Is()
함수에서는 다음과 같이 오류의 유형을 비교할 수 있습니다.
employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
fmt.Printf("NOT FOUND: %v\n", err)
} else {
fmt.Print(employee)
}
권장되는 오류 처리 방식
Go에서 오류를 처리할 때는 다음과 같은 권장 방식을 기억하세요.
- 오류가 예상되지 않는 경우에도 항상 오류가 있는지 검사합니다. 그런 다음 최종 사용자에게 불필요한 정보가 노출되지 않도록 오류를 올바르게 처리합니다.
- 오류가 어디에서 발생했는지 알 수 있도록 오류 메시지에 접두사를 포함합니다. 예를 들어, 패키지와 함수의 이름을 포함할 수 있습니다.
- 가능한 경우 항상 재사용 가능한 오류 변수를 만듭니다.
- 오류를 반환하는 것과 패닉의 차이점을 이해합니다. 패닉은 할 수 있는 것이 없을 때 적용하는 것입니다. 예를 들어, 종속성이 준비되지 않았다면(기본 동작을 실행하려는 경우가 아닌 이상) 프로그램이 작동할 수 없습니다.
- 오류는 가급적 많은 세부 정보와 함께 로깅하고(로깅은 다음 섹션에서 다룹니다), 최종 사용자가 이해할 수 있는 오류를 출력합니다.