12 minute read

개요

defer는 함수가 종료되기 직전에 특정 함수 호출을 지연시키는 키워드입니다.

주요 특징:

  • 지연 실행: 함수 반환 직전에 실행됨
  • LIFO 순서: 스택 구조로 나중에 defer된 것이 먼저 실행
  • 인자 즉시 평가: defer 문을 만나는 순간 인자가 평가됨
  • 패닉 시에도 실행: panic 발생 시에도 defer는 실행됨
  • 리소스 정리: 파일, 연결, 잠금 등의 정리에 필수적
  • 반환값 수정 가능: Named return value를 수정할 수 있음

defer 기본 동작

1. 단순 실행 순서

package main

import "fmt"

func basicDefer() {
    fmt.Println("Start")
    
    defer fmt.Println("Deferred 1")
    
    fmt.Println("Middle")
    
    defer fmt.Println("Deferred 2")
    
    fmt.Println("End")
}

func main() {
    basicDefer()
}

// 출력:
// Start
// Middle
// End
// Deferred 2  ← 나중에 defer된 것이 먼저
// Deferred 1

2. LIFO (Last In, First Out) 순서

func lifoOrder() {
    defer fmt.Println("First")
    defer fmt.Println("Second")
    defer fmt.Println("Third")
    defer fmt.Println("Fourth")
    
    fmt.Println("Function body")
}

func main() {
    lifoOrder()
}

// 출력:
// Function body
// Fourth  ← 마지막에 defer된 것부터
// Third
// Second
// First

3. 조건부 실행에서도 보장

func conditionalReturn(condition bool) {
    defer fmt.Println("Cleanup always runs")
    
    fmt.Println("Start")
    
    if condition {
        fmt.Println("Early return")
        return
    }
    
    fmt.Println("Normal end")
}

func main() {
    conditionalReturn(true)
    fmt.Println("---")
    conditionalReturn(false)
}

// 출력:
// Start
// Early return
// Cleanup always runs  ← return해도 실행됨
// ---
// Start
// Normal end
// Cleanup always runs

인자 평가 시점

1. 인자는 즉시 평가됨

func argumentEvaluation() {
    i := 0
    
    defer fmt.Println("Deferred:", i) // i의 값(0)이 즉시 캡처됨
    
    i++
    fmt.Println("Current:", i)
}

func main() {
    argumentEvaluation()
}

// 출력:
// Current: 1
// Deferred: 0  ← defer 시점의 값(0)이 출력됨

2. 포인터로 최신 값 참조

func pointerReference() {
    i := 0
    
    defer func() {
        fmt.Println("Deferred:", i) // 클로저로 i 참조
    }()
    
    i++
    fmt.Println("Current:", i)
}

func main() {
    pointerReference()
}

// 출력:
// Current: 1
// Deferred: 1  ← 최신 값(1)이 출력됨

3. 함수 호출 vs 클로저

func printValue(msg string, val int) {
    fmt.Printf("%s: %d\n", msg, val)
}

func evaluationComparison() {
    x := 10
    
    // 인자는 즉시 평가
    defer printValue("Direct", x)
    
    // 클로저는 실행 시점에 평가
    defer func() {
        printValue("Closure", x)
    }()
    
    x = 20
    fmt.Println("Changed x to:", x)
}

func main() {
    evaluationComparison()
}

// 출력:
// Changed x to: 20
// Closure: 20   ← 실행 시점의 값
// Direct: 10    ← defer 시점의 값

defer와 반환값

1. Named Return Value 수정

func modifyReturnValue() (result int) {
    defer func() {
        result++ // 반환값 수정 가능!
    }()
    
    return 5
}

func main() {
    fmt.Println(modifyReturnValue()) // 6 (5가 아님!)
}

2. 반환값 로깅

func divide(a, b float64) (result float64, err error) {
    defer func() {
        if err != nil {
            fmt.Printf("divide(%v, %v) failed: %v\n", a, b, err)
        } else {
            fmt.Printf("divide(%v, %v) = %v\n", a, b, result)
        }
    }()
    
    if b == 0 {
        err = fmt.Errorf("division by zero")
        return
    }
    
    result = a / b
    return
}

func main() {
    divide(10, 2)
    divide(10, 0)
}

// 출력:
// divide(10, 2) = 5
// divide(10, 0) failed: division by zero

3. 반환값 변환

func processData() (err error) {
    defer func() {
        if err != nil {
            // 에러를 더 구체적으로 변환
            err = fmt.Errorf("processData failed: %w", err)
        }
    }()
    
    // 작업 수행
    err = doSomething()
    return
}

func doSomething() error {
    return fmt.Errorf("something went wrong")
}

func main() {
    if err := processData(); err != nil {
        fmt.Println(err)
        // processData failed: something went wrong
    }
}

실전 활용 패턴

1. 파일 리소스 정리

import (
    "bufio"
    "fmt"
    "os"
)

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // ✅ 파일 자동 닫기
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    
    return scanner.Err()
}

func writeFile(filename string, data []string) error {
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    writer := bufio.NewWriter(file)
    defer writer.Flush() // ✅ 버퍼 플러시
    
    for _, line := range data {
        if _, err := writer.WriteString(line + "\n"); err != nil {
            return err
        }
    }
    
    return nil
}

2. Mutex 잠금 해제

import "sync"

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (sc *SafeCounter) Increment() {
    sc.mu.Lock()
    defer sc.mu.Unlock() // ✅ 자동 잠금 해제
    
    sc.count++
}

func (sc *SafeCounter) Value() int {
    sc.mu.Lock()
    defer sc.mu.Unlock()
    
    return sc.count
}

// RWMutex 예제
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock() // ✅ 읽기 잠금 해제
    
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock() // ✅ 쓰기 잠금 해제
    
    c.data[key] = value
}

3. 데이터베이스 트랜잭션

import "database/sql"

func transferMoney(db *sql.DB, from, to int, amount float64) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback() // 에러 시 롤백
        } else {
            tx.Commit() // 성공 시 커밋
        }
    }()
    
    // 출금
    _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
    if err != nil {
        return err
    }
    
    // 입금
    _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
    if err != nil {
        return err
    }
    
    return nil
}

4. HTTP 요청 본문 닫기

import (
    "io"
    "net/http"
)

func fetchURL(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close() // ✅ Body 자동 닫기
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    
    return string(body), nil
}

5. 시간 측정

import "time"

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)
    // 작업 수행
}

// 또는 직접 구현
func anotherFunction() {
    start := time.Now()
    defer func() {
        fmt.Printf("Execution time: %v\n", time.Since(start))
    }()
    
    time.Sleep(50 * time.Millisecond)
}

6. 패닉 복구

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic occurred: %v", r)
        }
    }()
    
    result = a / b // b가 0이면 패닉
    return
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        // Error: panic occurred: runtime error: integer divide by zero
    } else {
        fmt.Println("Result:", result)
    }
}

7. 상태 복원

func changeConfig(newValue string) {
    oldValue := config
    defer func() {
        config = oldValue // ✅ 원래 상태 복원
    }()
    
    config = newValue
    // 임시로 새 설정 사용
    doSomethingWithNewConfig()
}

var config string

func doSomethingWithNewConfig() {
    // 작업 수행
}

8. 로깅 및 추적

func trace(name string) func() {
    fmt.Printf("Entering %s\n", name)
    return func() {
        fmt.Printf("Exiting %s\n", name)
    }
}

func complexFunction() {
    defer trace("complexFunction")()
    
    fmt.Println("Doing work...")
    nestedFunction()
}

func nestedFunction() {
    defer trace("nestedFunction")()
    
    fmt.Println("Nested work...")
}

func main() {
    complexFunction()
}

// 출력:
// Entering complexFunction
// Doing work...
// Entering nestedFunction
// Nested work...
// Exiting nestedFunction
// Exiting complexFunction

9. 임시 디렉토리 정리

import (
    "io/ioutil"
    "os"
)

func processWithTempDir() error {
    tmpDir, err := ioutil.TempDir("", "myapp-")
    if err != nil {
        return err
    }
    defer os.RemoveAll(tmpDir) // ✅ 임시 디렉토리 삭제
    
    // tmpDir 사용
    fmt.Println("Using temp dir:", tmpDir)
    
    return nil
}

10. 연결 풀 반환

type Connection struct {
    id int
}

type Pool struct {
    conns chan *Connection
}

func (p *Pool) Get() *Connection {
    return <-p.conns
}

func (p *Pool) Put(conn *Connection) {
    p.conns <- conn
}

func useConnection(pool *Pool) error {
    conn := pool.Get()
    defer pool.Put(conn) // ✅ 연결 반환 보장
    
    // 연결 사용
    fmt.Printf("Using connection %d\n", conn.id)
    
    return nil
}

defer와 panic/recover

func recoverExample() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    fmt.Println("About to panic")
    panic("something went wrong")
    fmt.Println("This won't be printed")
}

func main() {
    recoverExample()
    fmt.Println("Program continues")
}

// 출력:
// About to panic
// Recovered from panic: something went wrong
// Program continues

중첩 defer와 panic

func nestedDefer() {
    defer fmt.Println("Defer 1")
    defer fmt.Println("Defer 2")
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    defer fmt.Println("Defer 3")
    
    panic("error!")
}

func main() {
    nestedDefer()
}

// 출력:
// Defer 3
// Recovered: error!
// Defer 2
// Defer 1

defer 루프에서 사용

1. 잘못된 사용 (메모리 누수)

func badLoopDefer() {
    for i := 0; i < 5; i++ {
        file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
        defer file.Close() // ❌ 함수 종료 시까지 모두 열려있음!
    }
    // 5개 파일이 모두 열린 채로 유지됨
}

2. 올바른 사용 (즉시 정리)

func goodLoopDefer() {
    for i := 0; i < 5; i++ {
        func() {
            file, _ := os.Open(fmt.Sprintf("file%d.txt", i))
            defer file.Close() // ✅ 익명 함수 종료 시 닫힘
            
            // 파일 처리
        }()
    }
}

// 또는 별도 함수로 분리
func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    // 파일 처리
    return nil
}

func goodLoopDefer2() {
    for i := 0; i < 5; i++ {
        processFile(fmt.Sprintf("file%d.txt", i))
    }
}

성능 고려사항

1. defer 오버헤드

import "testing"

func BenchmarkWithoutDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        increment()
    }
}

func BenchmarkWithDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        incrementWithDefer()
    }
}

var counter int

func increment() {
    counter++
}

func incrementWithDefer() {
    defer func() {}()
    counter++
}

// defer는 약간의 오버헤드 있음 (나노초 단위)
// 대부분의 경우 무시 가능
// 리소스 정리의 안전성이 더 중요

2. 핫 패스에서의 defer

// ❌ 성능 중요 루프에서 defer
func badHotPath(data []int) int {
    sum := 0
    for _, v := range data {
        func() {
            defer func() {}() // 불필요한 오버헤드
            sum += v
        }()
    }
    return sum
}

// ✅ 루프 밖에서 defer
func goodHotPath(data []int) (sum int) {
    defer func() {
        // 필요한 정리 작업
    }()
    
    for _, v := range data {
        sum += v
    }
    return
}

일반적인 실수

1. 루프에서 defer 남용

func mistake1() {
    // ❌ 모든 파일이 함수 종료까지 열려있음
    for i := 0; i < 100; i++ {
        f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
        defer f.Close()
    }
}

func fixed1() {
    // ✅ 각 반복마다 파일 닫힘
    for i := 0; i < 100; i++ {
        func(i int) {
            f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
            defer f.Close()
            // 파일 처리
        }(i)
    }
}

2. defer 인자 평가 시점 오해

func mistake2() {
    i := 0
    defer fmt.Println(i) // i의 값 0이 즉시 캡처됨
    
    i = 10
    // "10"이 아닌 "0"이 출력됨
}

func fixed2() {
    i := 0
    defer func() {
        fmt.Println(i) // 클로저로 최신 값 참조
    }()
    
    i = 10
    // "10"이 출력됨
}

3. 에러 무시

func mistake3(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // ❌ Close 에러 무시
    
    // 파일 처리
    return nil
}

func fixed3(filename string) (err error) {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer func() {
        if cerr := file.Close(); cerr != nil && err == nil {
            err = cerr // ✅ Close 에러 처리
        }
    }()
    
    // 파일 처리
    return nil
}

4. nil 포인터 defer

func mistake4(shouldOpen bool) error {
    var file *os.File
    var err error
    
    if shouldOpen {
        file, err = os.Open("test.txt")
        if err != nil {
            return err
        }
    }
    
    defer file.Close() // ❌ shouldOpen이 false면 nil.Close() 패닉!
    
    return nil
}

func fixed4(shouldOpen bool) error {
    var file *os.File
    var err error
    
    if shouldOpen {
        file, err = os.Open("test.txt")
        if err != nil {
            return err
        }
        defer file.Close() // ✅ 파일이 열렸을 때만 defer
    }
    
    return nil
}

5. defer로 변수 섀도잉

func mistake5() (err error) {
    defer func() {
        if err != nil {
            // err 처리
        }
    }()
    
    // ❌ 새로운 err 변수 생성 (섀도잉)
    file, err := os.Open("test.txt")
    if err != nil {
        return err // defer에서 캡처한 err는 여전히 nil
    }
    defer file.Close()
    
    return nil
}

func fixed5() (err error) {
    defer func() {
        if err != nil {
            // err 처리
        }
    }()
    
    // ✅ 기존 err 재사용
    var file *os.File
    file, err = os.Open("test.txt")
    if err != nil {
        return err
    }
    defer file.Close()
    
    return nil
}

6. recover 위치 오류

func mistake6() {
    // ❌ recover가 defer 안에 없으면 작동 안 함
    recover()
    panic("error")
}

func fixed6() {
    // ✅ defer 안에서 recover 호출
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    panic("error")
}

고급 패턴

1. 조건부 defer

func conditionalDefer(cleanup bool) {
    if cleanup {
        defer fmt.Println("Cleanup performed")
    }
    
    fmt.Println("Function body")
}

2. defer 스택 추적

import "runtime"

func stackTrace() {
    defer func() {
        if r := recover(); r != nil {
            buf := make([]byte, 1024)
            n := runtime.Stack(buf, false)
            fmt.Printf("Panic: %v\n%s\n", r, buf[:n])
        }
    }()
    
    causePanic()
}

func causePanic() {
    panic("something went wrong")
}

3. 메트릭 수집

type Metrics struct {
    mu            sync.Mutex
    requestCount  int
    totalDuration time.Duration
}

func (m *Metrics) recordRequest() func() {
    m.mu.Lock()
    m.requestCount++
    m.mu.Unlock()
    
    start := time.Now()
    return func() {
        duration := time.Since(start)
        m.mu.Lock()
        m.totalDuration += duration
        m.mu.Unlock()
    }
}

func handleRequest(m *Metrics) {
    defer m.recordRequest()()
    
    // 요청 처리
    time.Sleep(10 * time.Millisecond)
}

4. 리소스 풀 패턴

type ResourcePool struct {
    resources chan Resource
}

type Resource struct {
    id int
}

func (p *ResourcePool) Acquire() (*Resource, func()) {
    r := <-p.resources
    return r, func() {
        p.resources <- r
    }
}

func useResource(pool *ResourcePool) {
    resource, release := pool.Acquire()
    defer release()
    
    // 리소스 사용
    fmt.Printf("Using resource %d\n", resource.id)
}

5. 트랜잭션 래퍼

type Transaction struct {
    committed bool
}

func (t *Transaction) Commit() {
    t.committed = true
}

func (t *Transaction) Rollback() {
    // 롤백 로직
}

func beginTransaction() (*Transaction, func()) {
    tx := &Transaction{}
    return tx, func() {
        if !tx.committed {
            tx.Rollback()
        }
    }
}

func doWork() error {
    tx, cleanup := beginTransaction()
    defer cleanup()
    
    // 작업 수행
    if err := performWork(); err != nil {
        return err
    }
    
    tx.Commit()
    return nil
}

func performWork() error {
    return nil
}

defer vs 명시적 정리

// defer 사용 (권장)
func withDefer() error {
    file, err := os.Open("test.txt")
    if err != nil {
        return err
    }
    defer file.Close()
    
    // 여러 반환 경로가 있어도 안전
    if someCondition {
        return nil
    }
    
    if anotherCondition {
        return fmt.Errorf("error")
    }
    
    return nil
}

// 명시적 정리 (유지보수 어려움)
func withoutDefer() error {
    file, err := os.Open("test.txt")
    if err != nil {
        return err
    }
    
    if someCondition {
        file.Close() // 모든 경로에서 닫아야 함
        return nil
    }
    
    if anotherCondition {
        file.Close() // 빠뜨리기 쉬움
        return fmt.Errorf("error")
    }
    
    file.Close()
    return nil
}

var someCondition = false
var anotherCondition = false

정리

  • defer: 함수 종료 직전에 실행 보장
  • LIFO 순서: 나중에 defer된 것이 먼저 실행 (스택)
  • 인자 즉시 평가: defer 문을 만날 때 인자가 평가됨
  • 클로저로 최신 값: 익명 함수로 실행 시점 값 참조
  • Named return value: defer에서 반환값 수정 가능
  • 리소스 정리: 파일, 연결, 잠금 해제에 필수
  • Panic에도 실행: panic 발생 시에도 defer는 실행됨
  • 루프 주의: 루프에서는 익명 함수로 감싸기
  • 성능: 약간의 오버헤드 있지만 대부분 무시 가능
  • 에러 처리: Close 등의 에러도 적절히 처리
  • nil 체크: defer 전에 nil 체크 필수
  • recover: defer 안에서만 작동
  • 가독성과 안전성: 명시적 정리보다 defer가 더 안전