Go 에러 핸들링 패턴 정리

배경

사내 마이크로서비스 중 트래픽이 높은 API를 Node.js에서 Go로 전환하는 작업을 진행했다. 성능은 확실히 개선되었지만, 에러 핸들링 방식이 너무 달라서 초반에 혼란스러웠다.

기본 패턴

Go는 예외가 없고 에러를 반환값으로 처리한다.

func getUser(id string) (*User, error) {
    user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
    if err != nil {
        return nil, err
    }
    return user, nil
}

처음엔 if err != nil 구문이 반복되는 게 불편했지만, 오히려 에러 처리를 강제하니까 누락되는 케이스가 줄었다.

커스텀 에러 타입

비즈니스 로직에서 에러 구분이 필요할 때는 커스텀 타입을 사용했다.

type NotFoundError struct {
    Resource string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

func handleError(err error) {
    if _, ok := err.(*NotFoundError); ok {
        // 404 처리
        return
    }
    // 500 처리
}

에러 래핑

Go 1.13부터 fmt.Errorf로 에러 컨텍스트를 추가할 수 있다.

if err != nil {
    return fmt.Errorf("failed to get user %s: %w", userID, err)
}

%w 동사를 사용하면 원본 에러를 보존하면서 메시지를 추가할 수 있다.

소감

명시적 에러 처리가 장황하긴 하지만, 에러가 발생할 수 있는 지점이 코드에서 명확히 보인다는 장점이 있었다. 프로덕션에 배포 후 예상치 못한 에러가 확실히 줄었다.

Go 에러 핸들링 패턴 정리