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 동사를 사용하면 원본 에러를 보존하면서 메시지를 추가할 수 있다.
소감
명시적 에러 처리가 장황하긴 하지만, 에러가 발생할 수 있는 지점이 코드에서 명확히 보인다는 장점이 있었다. 프로덕션에 배포 후 예상치 못한 에러가 확실히 줄었다.