3. Errors Are Values
예외 대신 반환값으로 실패를 다루는 법
- Haram Lee
- 2026-10-05
- studies / Topics / Go
Go 코드를 처음 열면 if err != nil { return err }가 서너 줄마다 나온다. 예외가 없는 대신 실패를 반환값으로 돌려받기 때문이다. 이 글은 Go가 왜 이렇게 하는지, 에러를 감싸고 구분하는 표준 방법, 그리고 실제 서버 코드가 거기에 흔히 더하는 규칙을 정리한다.
3.1 Why Return Values, Not Exceptions
Go에서 실패할 수 있는 함수는 마지막 반환값으로 error를 돌려준다.
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open config: %w", err)
}
defer f.Close()예외는 호출 스택 어디서든 튀어나와서 “이 줄이 실패할 수 있는가"가 코드에 보이지 않는다. Go는 실패 가능성을 함수 시그니처와 호출 지점에 드러냈다. 코드는 장황해지지만 그 대신 어느 줄에서 무엇이 실패할 수 있고 실패하면 어떻게 하는지가 코드에 그대로 남는다. Scala의 Either나 Try를 반환하는 스타일과 생각은 같은데, 문법 지원 없이 if로 매번 확인한다는 점이 다르다.
error는 메서드 하나짜리 인터페이스다.
type error interface {
Error() string
}그래서 에러도 그냥 값이다. 변수에 담고, 비교하고, 감싸고, 직접 타입을 만들 수 있다.
3.2 Wrapping and Inspecting Errors
에러를 호출한 쪽으로 올려 보낼 때는 맥락을 붙여 감싼다. fmt.Errorf의 %w가 원래 에러를 안에 보관한다.
if err := producer.Produce(ctx, topic, b); err != nil {
return fmt.Errorf("produce to %s: %w", topic, err)
}이렇게 감싸면 최종 메시지는 produce to events: kafka: broker not available처럼 경로가 이어진다. 메시지에 “failed to"를 붙이지 않는 것이 관례다. 에러라는 사실은 읽는 사람이 이미 알기 때문이다.
감싼 뒤에도 원래 에러를 알아볼 수 있어야 한다. 이때 쓰는 함수가 표준 라이브러리 errors 패키지에 두 개 있다.
errors.Is(err, target): 감싼 사슬 안에 특정 에러 값이 있는지 본다. 미리 정의해 둔 에러(sentinel error)와 비교할 때 쓴다.errors.As(err, &target): 사슬 안에 특정 타입의 에러가 있으면 꺼낸다. 에러가 추가 정보(상태 코드 등)를 가진 struct일 때 쓴다.
var ErrInvalidEvent = errors.New("invalid event")
if errors.Is(err, ErrInvalidEvent) {
http.Error(w, "bad request", http.StatusBadRequest)
return
}err == ErrInvalidEvent로 직접 비교하면 감싼 에러에서는 실패한다. 항상 errors.Is를 쓴다.
3.3 defer, panic, and recover
defer: 함수가 끝날 때(정상이든 에러든) 실행할 코드를 등록한다. 파일 닫기, 락 풀기, 프로듀서 flush처럼 정리 작업에 쓴다. 등록한 역순으로 실행된다.panic: 복구할 수 없는 상황(프로그래머 실수, 불가능한 상태)에만 쓴다. 일반 실패에 쓰지 않는다.recover:defer안에서 panic을 잡는다. HTTP 서버는 핸들러 하나의 panic이 서버 전체를 죽이지 않도록 내부에서 recover한다.
3.4 Conventions on Top of the Standard Library
실제 서버 코드베이스는 표준 방식 위에 몇 가지 규칙을 더하는 경우가 많다. 자주 보이는 것을 추리면 이렇다.
- 변수 이름과 메시지를 맞춘다.
ErrSomethingBad = errors.New("something bad")처럼 이름만 보고 메시지를 알 수 있게 한다. - 처음 발생한 곳에서만 스택을 붙인다.
github.com/pkg/errors같은 라이브러리의errors.WithStack(ErrSomethingBad)로 한 번 감싸고, 그 위에서는 그대로 전파한다. 여러 층에서 감싸면 스택이 중복된다. - 핸들러는 상태 코드가 붙은 에러를 쓴다. gRPC 서버라면
status.Error(codes.Internal, "something wrong")처럼 상태 코드를 가진 에러를 만들어, 핸들러 경계에서 응답 코드로 바로 바꿀 수 있게 한다.
정리하면, 값으로 반환하고 %w로 감싸고 errors.Is로 구분하는 것이 표준 Go의 기본이다. 실무 코드는 여기에 스택 추적과 상태 코드를 더한다.