본문으로 건너뛰기

실전 고수 팁 — 에러 처리

Go의 에러 처리 철학

package main

import (
"errors"
"fmt"
)

// 철학 1: 에러는 값이다 (Errors are values)
// 에러를 특별히 취급하지 말고 일반 값처럼 다룬다

type Result[T any] struct {
Value T
Err error
}

func NewResult[T any](val T, err error) Result[T] {
return Result[T]{Value: val, Err: err}
}

func (r Result[T]) IsOK() bool { return r.Err == nil }

func (r Result[T]) Unwrap() (T, error) { return r.Value, r.Err }

// 철학 2: 실패를 처리하라, 무시하지 마라
func badStyle() {
// Bad: 에러 무시
// result, _ := someOperation()
// 에러가 발생해도 알 수 없음
}

// 철학 3: 에러 컨텍스트를 풍부하게
var ErrNotFound = errors.New("not found")

// Bad: 어디서 발생했는지 모름
func badGetUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "user", nil
}

// Good: 컨텍스트 포함
func goodGetUser(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("GetUser(id=%d): %w", id, ErrNotFound)
}
return "user", nil
}

func main() {
// 에러를 값으로 다루기
results := []Result[int]{
NewResult(42, nil),
NewResult(0, fmt.Errorf("calculation failed: %w", ErrNotFound)),
}

for _, r := range results {
val, err := r.Unwrap()
if err != nil {
fmt.Println("에러:", err)
if errors.Is(err, ErrNotFound) {
fmt.Println("→ 찾을 수 없음")
}
} else {
fmt.Println("값:", val)
}
}

// 컨텍스트 비교
_, err1 := badGetUser(-1)
_, err2 := goodGetUser(-1)
fmt.Printf("\n나쁜 에러: %v\n좋은 에러: %v\n", err1, err2)
}

if err != nil 줄이기 패턴

package main

import (
"fmt"
"strconv"
)

// 패턴 1: errWriter — io.Writer와 유사하게 에러를 한번에 처리
type errWriter struct {
data []byte
err error
}

func (ew *errWriter) writeString(s string) {
if ew.err != nil {
return // 이미 에러가 있으면 스킵
}
ew.data = append(ew.data, s...)
}

func (ew *errWriter) writeInt(n int) {
if ew.err != nil {
return
}
ew.data = append(ew.data, strconv.Itoa(n)...)
}

func (ew *errWriter) validate(cond bool, msg string) {
if ew.err != nil {
return
}
if !cond {
ew.err = fmt.Errorf("validation: %s", msg)
}
}

func buildPayload(name string, age int) ([]byte, error) {
ew := &errWriter{}
ew.validate(name != "", "name is required")
ew.validate(age >= 0, "age must be non-negative")
ew.validate(age <= 150, "age is too large")
ew.writeString(`{"name":"`)
ew.writeString(name)
ew.writeString(`","age":`)
ew.writeInt(age)
ew.writeString(`}`)
return ew.data, ew.err
}

// 패턴 2: 함수형 체이닝
type Pipeline[T any] struct {
value T
err error
}

func NewPipeline[T any](val T) *Pipeline[T] {
return &Pipeline[T]{value: val}
}

func (p *Pipeline[T]) Then(fn func(T) (T, error)) *Pipeline[T] {
if p.err != nil {
return p
}
p.value, p.err = fn(p.value)
return p
}

func (p *Pipeline[T]) Result() (T, error) {
return p.value, p.err
}

// 패턴 3: defer로 에러 처리 통합
func processWithDefer(id int) (result string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("processWithDefer(id=%d): %w", id, err)
}
}()

if id <= 0 {
return "", fmt.Errorf("invalid id")
}
return fmt.Sprintf("processed-%d", id), nil
}

func main() {
// errWriter 패턴
fmt.Println("=== errWriter 패턴 ===")
data, err := buildPayload("Alice", 30)
if err == nil {
fmt.Println("페이로드:", string(data))
}

_, err = buildPayload("", -1)
if err != nil {
fmt.Println("유효성 실패:", err)
}

// Pipeline 패턴
fmt.Println("\n=== Pipeline 패턴 ===")
result, err := NewPipeline(" hello world ").
Then(func(s string) (string, error) {
if s == "" {
return "", fmt.Errorf("empty string")
}
// 트림
trimmed := ""
for i, c := range s {
if c != ' ' {
if trimmed == "" {
trimmed = s[i:]
}
}
}
return "trimmed: " + s, nil
}).
Then(func(s string) (string, error) {
return "[" + s + "]", nil
}).
Result()

if err != nil {
fmt.Println("파이프라인 에러:", err)
} else {
fmt.Println("파이프라인 결과:", result)
}

// defer 에러 래핑
fmt.Println("\n=== defer 에러 래핑 ===")
for _, id := range []int{1, -1} {
res, err := processWithDefer(id)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("결과:", res)
}
}
}

slog를 이용한 구조화 로깅 연동

package main

import (
"errors"
"fmt"
"log/slog"
"os"
)

// 에러 코드 타입
type AppError struct {
Code int
Message string
Err error
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error { return e.Err }

// slog와 에러 연동 패턴
func logError(logger *slog.Logger, err error, msg string, args ...any) {
if err == nil {
return
}

// AppError에서 코드 추출
var appErr *AppError
if errors.As(err, &appErr) {
logger.Error(msg,
append(args,
"error", err,
"code", appErr.Code,
"cause", appErr.Err,
)...,
)
return
}

logger.Error(msg, append(args, "error", err)...)
}

var ErrNotFound = errors.New("not found")

func fetchUser(id int) (string, error) {
if id <= 0 {
return "", &AppError{Code: 400, Message: "invalid user id"}
}
if id > 100 {
return "", &AppError{Code: 404, Message: "user not found", Err: ErrNotFound}
}
return fmt.Sprintf("User-%d", id), nil
}

func main() {
// JSON 형식의 구조화 로거
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))

// 텍스트 형식 (개발용)
textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))

ids := []int{1, 0, 999}
for _, id := range ids {
user, err := fetchUser(id)
if err != nil {
logError(logger, err, "사용자 조회 실패", "user_id", id)
logError(textLogger, err, "사용자 조회 실패", "user_id", id)
} else {
logger.Info("사용자 조회 성공", "user_id", id, "user", user)
}
}
}

에러 처리 안티패턴과 올바른 패턴

package main

import (
"errors"
"fmt"
)

var ErrNotFound = errors.New("not found")

// ====== 안티패턴 ======

// 안티패턴 1: 에러 무시
func antiPattern1() {
val := 0
_ = val // 에러를 완전히 무시
// 올바른 방법: 에러를 처리하거나 상위로 전파
}

// 안티패턴 2: panic을 일반 에러 제어로 사용
func antiPattern2(id int) string {
if id <= 0 {
panic("invalid id") // Bad! 일반 에러는 error 반환으로
}
return fmt.Sprintf("item-%d", id)
}

// 안티패턴 3: 에러를 문자열로 비교
func antiPattern3(err error) bool {
return err.Error() == "not found" // Bad! 문자열 비교는 깨지기 쉬움
}

// 안티패턴 4: 에러 컨텍스트 없이 그냥 전파
func antiPattern4() error {
err := ErrNotFound
return err // Bad! 어디서 발생했는지 알 수 없음
}

// ====== 올바른 패턴 ======

// 올바른 패턴 1: 에러 전파 + 컨텍스트
func goodPattern1(id int) (string, error) {
if id <= 0 {
return "", fmt.Errorf("goodPattern1: invalid id %d: %w", id, ErrNotFound)
}
return fmt.Sprintf("item-%d", id), nil
}

// 올바른 패턴 2: errors.Is로 비교
func goodPattern2(err error) bool {
return errors.Is(err, ErrNotFound) // Good!
}

// 올바른 패턴 3: 에러 타입 확인은 errors.As
type ValidationError struct{ Field, Msg string }

func (e *ValidationError) Error() string {
return fmt.Sprintf("validation[%s]: %s", e.Field, e.Msg)
}

func goodPattern3(err error) {
var ve *ValidationError
if errors.As(err, &ve) { // Good! 타입 안전 추출
fmt.Printf("필드 %q 유효성 실패: %s\n", ve.Field, ve.Msg)
}
}

// 올바른 패턴 4: 에러 조기 반환 (early return)
func goodPattern4(a, b, c int) (int, error) {
// 각 단계에서 즉시 반환 — 들여쓰기 줄이기
if a <= 0 {
return 0, fmt.Errorf("a must be positive: %d", a)
}
if b <= 0 {
return 0, fmt.Errorf("b must be positive: %d", b)
}
if c <= 0 {
return 0, fmt.Errorf("c must be positive: %d", c)
}
return a + b + c, nil
}

// 올바른 패턴 5: 에러 그룹화 처리 (Go 1.20+)
func goodPattern5(ids []int) error {
var errs []error
for _, id := range ids {
if id <= 0 {
errs = append(errs, fmt.Errorf("invalid id: %d", id))
}
}
return errors.Join(errs...)
}

func main() {
// 패턴 비교
fmt.Println("=== 에러 전파 ===")
_, err := goodPattern1(-1)
fmt.Println("에러:", err)
fmt.Println("Is ErrNotFound:", goodPattern2(err))

fmt.Println("\n=== 타입 안전 에러 확인 ===")
ve := &ValidationError{Field: "email", Msg: "invalid format"}
wrapped := fmt.Errorf("process: %w", ve)
goodPattern3(wrapped)

fmt.Println("\n=== early return ===")
result, err := goodPattern4(1, 0, 3)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("결과:", result)
}

fmt.Println("\n=== 에러 그룹화 ===")
joinErr := goodPattern5([]int{1, -1, 0, 2, -3})
if joinErr != nil {
fmt.Println("복합 에러:", joinErr)
}

fmt.Println("\n=== 요약 ===")
fmt.Println("1. 에러를 무시하지 말 것 (_ 사용 최소화)")
fmt.Println("2. 비교는 errors.Is, 타입 추출은 errors.As")
fmt.Println("3. %w로 래핑하여 컨텍스트 보존")
fmt.Println("4. 일반 에러에 panic 사용 금지")
fmt.Println("5. early return으로 들여쓰기 최소화")
}

핵심 규칙

  1. 에러는 값이다— 특별 취급 말고 일반 값처럼 처리
  2. 에러를 무시하지 마라_로 버리는 것은 버그의 시작
  3. %w로 컨텍스트 추가— 어디서 왜 실패했는지 기록
  4. errors.Is / errors.As— 문자열 비교 대신 타입 안전 비교
  5. panic은 최후의 수단— 일반 에러 상황에 사용 금지
  6. 조기 반환(early return)— 들여쓰기를 줄이고 가독성 향상
  7. slog로 구조화 로깅— 에러 코드와 컨텍스트를 함께 기록