본문으로 건너뛰기
Previous
Next
[Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라

[Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라

[Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라

이 글의 핵심

C++ try-catch 대신 Go의 명시적 에러 핸들링을 배웁니다. 다중 반환값, if err != nil 패턴, defer로 자원 해제, panic/recover의 올바른 사용법까지 실전 예제로 학습합니다.

시리즈 안내

📚 Go 2주 완성 시리즈 #05 | 전체 목차 보기

이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼Day 8~9 내용입니다.

이전: #04 인터페이스 ← | → 다음: #06 고루틴·채널


💡 초보자를 위한 한 줄: Go에는 try-catch가 없습니다. 에러는 error 타입으로 반환하고 if err != nil즉시 처리합니다. defer는 “함수 끝날 때 실행”으로 파일 닫기/락 해제에 씁니다. panic은 C++의 abort()처럼 “프로그램 죽이기”이므로 웬만하면 안 씁니다.

들어가며: “try-catch 없이 어떻게 에러 처리를 하죠?”

C++에서는 try-catch로 에러를 한 곳에서 몰아 처리했습니다:

try {
    openFile();
    processData();
    saveResult();
} catch (const std::exception& e) {
    // 어디서 에러가 났는지는 스택 추적으로 확인
}

편리하지만, 코드만 봐서는 어디서 예외가 발생할지 알기 어렵습니다.

Go는 완전히 다릅니다. 예외가 없습니다. 대신 에러를 값으로 반환하고, 호출한 곳에서 즉시 처리합니다:

file, err := os.Open("data.txt")
if err != nil {
    return fmt.Errorf("파일 열기 실패: %w", err)
}
defer file.Close()

처음에는 if err != nil이 계속 반복되어 번거로워 보이지만, 코드의 흐름이 명확해지고 “예상치 못한 예외로 프로그램이 죽는 일”이 줄어듭니다. 이 글에서 배울 내용:

  • 다중 반환값으로 에러 전달
  • if err != nil 패턴
  • defer로 자원 해제 보장
  • errors.New·fmt.Errorf·%w, Sentinel, errors.Is/As
  • panicrecover의 올바른 사용

C++ 개발자 관점: C++ 백그라운드에서 Go로 전환하며 겪은 차이점과 함정을 중심으로 설명합니다. 포인터, 동시성, 메모리 관리 등 핵심 개념을 비교하며 정리했습니다.

실무에서의 체감

C++ 위주로 서버를 다루던 환경에서 Go를 도입할 때 흔히 드는 인상은 문법과 툴체인이 단순해 보인다는 점입니다. 프로덕션에서는 그 단순함이 빌드·배포·동시성 코드 가독성으로 이어지는 경우가 많습니다. 자주 언급되는 장점:

  • 개발 속도: 팀·도메인에 따라 다르지만, 네트워크·CLI 코드를 빠르게 완성하기 쉬운 편입니다.
  • 안정성: GC가 있어 수동 할당 해제 부담이 줄어듭니다.
  • 배포: 단일 바이너리로 옮기기 쉬운 구조입니다.

1. 에러를 값으로: error 타입

C++ vs Go: 에러 처리 방식

// C++: 예외로 에러 처리
#include <iostream>
#include <fstream>
#include <stdexcept>
void readFile(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("Failed to open file");
    }
    
    std::string line;
    while (std::getline(file, line)) {
        if (line.empty()) {
            throw std::invalid_argument("Empty line found");
        }
        std::cout << line << "\n";
    }
}
int main() {
    try {
        readFile("data.txt");
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}
// Go: error 반환으로 에러 처리
package main
import (
    "bufio"
    "fmt"
    "os"
)
func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            return fmt.Errorf("empty line found")
        }
        fmt.Println(line)
    }
    
    if err := scanner.Err(); err != nil {
        return fmt.Errorf("scan error: %w", err)
    }
    
    return nil
}
func main() {
    if err := readFile("data.txt"); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

핵심 차이점:

  • Go는 예외를 던지지 않습니다
  • 에러는 반환값으로 전달됩니다
  • 호출자는 반드시 에러를 확인해야 합니다

error 인터페이스

// Go: error는 인터페이스
type error interface {
    Error() string
}
// 커스텀 에러 타입
type FileError struct {
    Path string
    Op   string
    Err  error
}
func (e *FileError) Error() string {
    return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)
}
// 사용
func openFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return &FileError{
            Path: path,
            Op:   "open",
            Err:  err,
        }
    }
    return nil
}

2. if err != nil 패턴

기본 패턴

// Go: 가장 흔한 패턴
func doSomething() error {
    result, err := someOperation()
    if err != nil {
        return err  // 에러 전파
    }
    
    // result 사용
    return nil
}

에러 처리 변형

// Go: 다양한 에러 처리 패턴
package main
import (
    "fmt"
    "os"
)
// 1. 즉시 반환
func pattern1() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return err
    }
    defer f.Close()
    // ...
    return nil
}
// 2. 로깅 후 반환
func pattern2() error {
    f, err := os.Open("file.txt")
    if err != nil {
        fmt.Println("Failed to open file:", err)
        return err
    }
    defer f.Close()
    // ...
    return nil
}
// 3. 기본값 사용
func pattern3() int {
    data, err := fetchData()
    if err != nil {
        return 0  // 기본값
    }
    return data
}
// 4. 재시도
func pattern4() error {
    maxRetries := 3
    for i := 0; i < maxRetries; i++ {
        err := tryOperation()
        if err == nil {
            return nil
        }
        fmt.Printf("Attempt %d failed: %v\n", i+1, err)
    }
    return fmt.Errorf("failed after %d retries", maxRetries)
}

3. defer: RAII의 대체재

C++ RAII vs Go defer

// C++: RAII로 자동 정리
void processFile() {
    std::ifstream file("data.txt");
    std::lock_guard<std::mutex> lock(mtx);
    
    // 작업...
    
    // 스코프 종료 시 자동으로:
    // 1. lock 해제 (lock_guard 소멸자)
    // 2. file 닫기 (ifstream 소멸자)
}
// Go: defer로 명시적 정리
func processFile() error {
    file, err := os.Open("data.txt")
    if err != nil {
        return err
    }
    defer file.Close()  // 함수 종료 시 실행
    
    mu.Lock()
    defer mu.Unlock()   // 함수 종료 시 실행
    
    // 작업...
    
    return nil
    // 여기서 defer들이 역순으로 실행:
    // 1. mu.Unlock()
    // 2. file.Close()
}

defer의 실행 순서 (LIFO)

// Go: defer는 LIFO (Last In First Out)
package main
import "fmt"
func example() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    
    fmt.Println("함수 본문")
}
func main() {
    example()
}
// 출력:
// 함수 본문
// 3
// 2
// 1

defer의 평가 시점

// Go: defer의 인자는 즉시 평가됨
package main
import "fmt"
func example() {
    x := 1
    defer fmt.Println("Deferred:", x)  // x=1이 즉시 평가됨
    
    x = 2
    fmt.Println("Current:", x)
}
func main() {
    example()
}
// 출력:
// Current: 2
// Deferred: 1  (defer 등록 시점의 값)

함수 호출로 지연 평가:

// Go: 함수로 감싸서 지연 평가
func example() {
    x := 1
    defer func() {
        fmt.Println("Deferred:", x)  // 실행 시점의 x 사용
    }()
    
    x = 2
    fmt.Println("Current:", x)
}
// 출력:
// Current: 2
// Deferred: 2  (실행 시점의 값)

defer 활용 패턴

// Go: defer 실전 패턴
package main
import (
    "fmt"
    "time"
)
// 1. 실행 시간 측정
func measureTime(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("%s took %v\n", name, time.Since(start))
    }
}
func slowFunction() {
    defer measureTime("slowFunction")()  // 함수 반환값(함수)를 defer
    
    time.Sleep(100 * time.Millisecond)
}
// 2. 락 보호
func criticalSection() {
    mu.Lock()
    defer mu.Unlock()
    
    // 여러 return 경로가 있어도 unlock 보장
    if condition1 {
        return
    }
    if condition2 {
        return
    }
    // ...
}
// 3. 트랜잭션 롤백
func transaction() error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    
    // 작업...
    if err := doWork(tx); err != nil {
        return err  // defer에서 Rollback 실행
    }
    
    return tx.Commit()
}

4. 에러 래핑과 언래핑

에러 래핑 (Error Wrapping)

// Go: 에러 래핑으로 컨텍스트 추가
package main
import (
    "fmt"
    "os"
)
func readConfig(path string) error {
    data, err := os.ReadFile(path)
    if err != nil {
        // %w로 원본 에러 래핑
        return fmt.Errorf("read config from %s: %w", path, err)
    }
    
    if len(data) == 0 {
        return fmt.Errorf("config file %s is empty", path)
    }
    
    return nil
}
func loadSettings() error {
    if err := readConfig("config.json"); err != nil {
        return fmt.Errorf("load settings: %w", err)
    }
    return nil
}
func main() {
    if err := loadSettings(); err != nil {
        fmt.Println("Error:", err)
        // 출력: Error: load settings: read config from config.json: open config.json: no such file or directory
    }
}

errors.Is와 errors.As

// Go: 특정 에러 확인
package main
import (
    "errors"
    "fmt"
    "os"
)
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)
func findUser(id int) error {
    if id < 0 {
        return ErrNotFound
    }
    return nil
}
func main() {
    err := findUser(-1)
    
    // errors.Is로 에러 확인
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found")
    }
}
// Go: errors.As로 타입 확인
package main
import (
    "errors"
    "fmt"
    "os"
)
func openFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("open file: %w", err)
    }
    return nil
}
func main() {
    err := openFile("nonexistent.txt")
    
    // errors.As로 특정 타입 추출
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Println("Path error:")
        fmt.Println("  Op:", pathErr.Op)
        fmt.Println("  Path:", pathErr.Path)
        fmt.Println("  Err:", pathErr.Err)
    }
}

fmt.Errorf%w를 쓰면 에러 체인이 만들어져, 여러 겹 감싼 뒤에도 errors.Is(err, os.ErrNotExist)처럼 근본 원인을 찾을 수 있습니다. 반면 %v만 쓰면 문자열로만 붙고 Is/As가 따라가지 못합니다.

5. errors.New vs fmt.Errorf, Sentinel, 실전 래핑

errors.New vs fmt.Errorf

API쓰는 때메모
errors.New("msg")문맥 없는 고정 메시지의 단순 에러, Sentinel로 패키지 전역 변수에 두고 errors.Is로 비교포맷 불가 — 문자열 하나
fmt.Errorf("format", args...)동적 값을 문자열에 넣을 때포맷만 하고 끝내면 래핑 아님
fmt.Errorf("...: %w", err)원인 에러를 체인에 남길 때(Go 1.13+)Unwrap 가능 → errors.Is/As가 안쪽까지 따라감
package example
import (
    "errors"
    "fmt"
)
var ErrNotFound = errors.New("not found") // Sentinel — 같은 변수로 Is 비교
func badUser(id int) error {
    return fmt.Errorf("user %d not found", id) // OK이지만 Is(ErrNotFound)와 연결하려면 %w 필요
}
func goodUser(id int) error {
    return fmt.Errorf("user %d: %w", id, ErrNotFound) // 체인에 ErrNotFound 포함
}

커스텀 에러 타입과 errors.As

  • errors.Is값이 같은지(Sentinel 또는 Unwrap 체인) 볼 때 쓰고,
  • errors.As구체 타입으로 필드(예: HTTP 상태 코드, 에러 코드)를 꺼낼 때 씁니다.
  • 커스텀 타입은 Error() string만 구현해도 되고, 필요하면 Unwrap() error를 추가해 한 겹 더 감쌀 수 있습니다.
package example
import "fmt"
type AppError struct {
    Code    int
    Message string
    Err     error
}
func (e *AppError) Error() string {
    return fmt.Sprintf("code=%d: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error { return e.Err }

Sentinel 에러(전역 비교용) 실무 패턴

  • 패키지 상단에 var ErrXxx = errors.New(...) 로 선언해 의미를 고정합니다.
  • 호출부는 errors.Is(err, pkg.ErrNotFound)처럼 비교하고, 중간 함수들은 fmt.Errorf(“context: %w”, err)로 맥락만 덧붙입니다.
  • 같은 문장 문자열을 여러 번 errors.New로 만들면 Is가 실패합니다. 반드시 공유된 변수를 쓰세요.

에러 래핑 실전 체크리스트

  1. 감싸기: 실패 지점에서 return fmt.Errorf("작업 X: %w", err).
  2. 판별: 바깥에서는 errors.Is / errors.As만 사용(문자열 Contains 비교는 최후 수단).
  3. 로그: 최종적으로 사용자·운영에 남길 때 한 번만 상세 스택이 필요한지 팀 규칙으로 정합니다.

6. panic과 recover

panic: 복구 불가능한 오류

// C++: 예외 던지기
void divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("division by zero");
    }
    std::cout << a / b << "\n";
}
// Go: panic (일반적으로 지양)
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // 프로그램 종료
    }
    return a / b
}
// ✅ 권장: error 반환
func divideWithError(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

recover: panic 복구

// Go: recover로 panic 복구
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    
    result = a / b  // b=0이면 panic
    return result, nil
}
func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

panic/recover는 언제 쓰나

상황권장
일반적인 실패(파일 없음, 네트워크 타임아웃, 잘못된 입력)error 반환
프로그래밍 오류(불변식 위반, nil 역참조, 도달하면 안 되는 분기)패닉 가능 — 가능하면 테스트로 걸러냄
초기화 시 반드시 성공해야 하는 값(정규식 MustCompile, template.Must)panic 허용되는 관용구
HTTP 서버 등에서 한 요청만 격리recover고루틴 경계에서만 의미 있음 — 미들웨어·핸들러 상단 패턴(#06 이후와 연결)
recover지연 함수(defer) 안에서만 동작합니다. “조용히 삼키기”보다는 로그 남기고 error로 변환하거나 요청 단위로 500을 반환하는 식이 일반적입니다.

panic 사용 가이드

// ❌ 나쁜 예: 일반 에러에 panic 사용
func fetchUser(id int) *User {
    user, err := db.Query(id)
    if err != nil {
        panic(err)  // 나쁨!
    }
    return user
}
// ✅ 좋은 예: error 반환
func fetchUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("fetch user %d: %w", id, err)
    }
    return user, nil
}
// ✅ panic이 적절한 경우: 프로그래밍 오류
func mustCompile(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(fmt.Sprintf("invalid regex pattern: %s", pattern))
    }
    return re
}
// 초기화 시 사용 (프로그램 시작 시 실패해야 함)
var emailRegex = mustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)

7. 실습 과제

과제 1: 파일 복사 with defer

// Go: defer로 안전한 파일 복사
package main
import (
    "fmt"
    "io"
    "os"
)
func copyFile(src, dst string) error {
    // 원본 파일 열기
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("open source: %w", err)
    }
    defer srcFile.Close()  // 반드시 닫힘
    
    // 대상 파일 생성
    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("create destination: %w", err)
    }
    defer dstFile.Close()  // 반드시 닫힘
    
    // 복사
    _, err = io.Copy(dstFile, srcFile)
    if err != nil {
        return fmt.Errorf("copy: %w", err)
    }
    
    return nil
}
func main() {
    if err := copyFile("source.txt", "dest.txt"); err != nil {
        fmt.Fprintf(os.Stderr, "Copy failed: %v\n", err)
        os.Exit(1)
    }
    fmt.Println("Copy successful")
}

과제 2: 커스텀 에러 타입

// Go: 커스텀 에러 타입
package main
import (
    "errors"
    "fmt"
)
type ValidationError struct {
    Field   string
    Value   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error [%s=%s]: %s", 
        e.Field, e.Value, e.Message)
}
func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Value:   email,
            Message: "email is required",
        }
    }
    
    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Value:   email,
            Message: "invalid email format",
        }
    }
    
    return nil
}
func registerUser(email string) error {
    if err := validateEmail(email); err != nil {
        return fmt.Errorf("register user: %w", err)
    }
    
    // 등록 로직...
    return nil
}
func main() {
    err := registerUser("invalid")
    
    // 타입 확인
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Println("Validation failed:")
        fmt.Println("  Field:", validationErr.Field)
        fmt.Println("  Value:", validationErr.Value)
        fmt.Println("  Message:", validationErr.Message)
    }
}

과제 3: 에러 체인

// Go: 에러 체인 추적
package main
import (
    "errors"
    "fmt"
)
var (
    ErrDatabase   = errors.New("database error")
    ErrConnection = errors.New("connection error")
)
func connectDB() error {
    return ErrConnection
}
func queryUser(id int) error {
    if err := connectDB(); err != nil {
        return fmt.Errorf("query user %d: %w", id, err)
    }
    return nil
}
func getProfile(id int) error {
    if err := queryUser(id); err != nil {
        return fmt.Errorf("get profile: %w", err)
    }
    return nil
}
func main() {
    err := getProfile(123)
    
    fmt.Println("Error:", err)
    // 출력: Error: get profile: query user 123: connection error
    
    // 원본 에러 확인
    if errors.Is(err, ErrConnection) {
        fmt.Println("Root cause: connection error")
    }
}

과제 4: defer로 실행 시간 측정

// Go: defer로 함수 실행 시간 측정
package main
import (
    "fmt"
    "time"
)
func trace(name string) func() {
    start := time.Now()
    fmt.Printf("Entering %s\n", name)
    
    return func() {
        fmt.Printf("Exiting %s (took %v)\n", name, time.Since(start))
    }
}
func processData() {
    defer trace("processData")()
    
    fmt.Println("Processing...")
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Done")
}
func main() {
    processData()
}
// 출력:
// Entering processData
// Processing...
// Done
// Exiting processData (took 100ms)

정리: Day 8~9 학습 체크리스트

완료해야 할 항목

  • error 타입과 if err != nil 패턴 숙지
  • 다중 반환값으로 에러 전달
  • defer로 자원 해제 보장 (RAII 대체)
  • defer의 LIFO 실행 순서 이해
  • fmt.Errorf%w로 에러 래핑
  • errors.New(Sentinel) vs fmt.Errorf·%w 구분
  • errors.Is, errors.As로 에러 검사
  • panic/recover는 제한적으로만 사용
  • 실습 과제 4개 완료

C++에서 Go로 전환 포인트

C++Go비고
throwreturn error명시적
try-catchif err != nil매 호출마다
RAIIdefer함수 단위
예외 전파에러 반환 체인명확한 흐름
std::exceptionerror 인터페이스더 간결

다음 단계 예고

Day 8~9에서는 Go의 에러 처리를 배웠습니다. 다음 글에서는 Go의 진짜 강점인 고루틴과 채널을 다룹니다. C++ 개발자가 Go에 열광하는 이유를 직접 체험하게 됩니다!

📚 시리즈 네비게이션

이전 글목차다음 글
← #04 인터페이스📑 전체 목차#06 고루틴·채널 →
Go 2주 완성 시리즈:
커리큘럼#01 기본 문법#02 자료구조#03 객체지향#04 인터페이스#05 에러 처리#06 고루틴·채널#07 테스팅#08 REST API#09 context·우아한 종료

한 줄 요약: Go는 예외 대신 명시적 에러 반환으로 코드 흐름을 명확하게 하고, defer로 자원 해제를 보장합니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [Go 2주 완성 #04] Day 7: 다형성의 재해석, 인터페이스 - 가상 함수 없이 다형성 구현하기
  • C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
  • C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지

이 글에서 다루는 키워드 (관련 검색어)

Go error 처리, if err != nil, defer Go, panic recover, fmt.Errorf, C++ 예외 비교, Golang 에러 핸들링, Go 2주 완성 등으로 검색하시면 이 글이 도움이 됩니다.

실전 팁 (Go)

  • go fmt, go vet, 필요 시 golangci-lint로 기본 품질을 맞춥니다.
  • if err != nil을 습관화합니다. 에러를 삼키면 운영에서 원인 추적이 어렵습니다.
  • go test ./...로 패키지 단위 회귀를 확인합니다. 성능은 go test -bench측정할 수 있을 때만 다룹니다.

실전 체크리스트 (Go)

코드

  • 외부 호출·I/O 실패 시 에러가 처리되거나 로깅되는가?
  • 고루틴·채널 사용 시 블로킹·누수 가능성을 검토했는가?

모듈

  • go.mod / go.sum이 팀과 합의된 범위인가?

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ try-catch 대신 Go의 명시적 에러 핸들링을 배웁니다. 다중 반환값, if err != nil 패턴, defer로 자원 해제, panic/recover의 올바른 사용법까지 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「[Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「[Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.