Go의 비밀스러운 삶: ‘defer’ 문

발행: (2026년 2월 9일 오후 01:44 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

Source:

Chapter 20: The Stacked Deck

Ethan의 데스크톱 PC 팬이 크게 돌고 있었다. 그는 터미널에 쏟아지는 오류 메시지를 바라보고 있었는데, 마치 고장 난 소화전처럼 연속으로 출력되고 있었다.

panic: too many open files
panic: too many open files

“왜 이럴까,” 그는 Ctrl+C 를 눌러 서버를 강제 종료하며 중얼거렸다. “다 닫았어. 세 번이나 확인했는데.”

Eleanor가 차 트레이를 들고 지나갔다. “ExportData 함수 보여줘.”

Ethan이 코드를 열었다. 파일을 열고, 데이터베이스를 조회하고, CSV에 쓰고, 마지막으로 S3에 업로드하는 50줄 정도 되는 긴 함수였다.

func ExportData(id string) error {
    f, err := os.Open("data.csv")
    if err != nil {
        return err
    }

    db, err := sql.Open("postgres", "...")
    if err != nil {
        return err // <--- The Leak
    }

    // ... 40 lines of logic ...

    db.Close()
    f.Close() // <--- The Cleanup
    return nil
}

“파일은 바로 아래에서 닫아,” Ethan이 가리켰다.

“그런데 데이터베이스 연결에 실패하면 어떻게 돼?” Eleanor가 부드럽게 물었다.

Ethan은 두 번째 if err != nil 블록을 바라보았다. “오류를 반환하죠.”

“그럼 반환하기 전에 파일을 닫나요?”

Ethan은 얼어붙었다. “아니요. 바로 반환합니다. 파일은 열려 있는 채로 남아요.”

“바로 그거야. 그리고 논리 40줄 중간에 오류가 발생하면? 파일은 열려 있어. 함수가 panic 하면? 파일은 열려 있어. 바로 누수가 생긴 거야.”

“다른 언어에서는,” Eleanor가 설명했다, “try...finally 블록으로 감싸곤 하지. 정리 코드를 맨 아래, 자원을 연 곳과 멀리 떨어진 곳에 두고, 스크롤을 내려서 확인해야 해.”

“Go에서는 Proximity(근접성) 를 선호해.”

그녀는 키보드를 잡고 정리 코드를 옮겼다.

func ExportData(id string) error {
    f, err := os.Open("data.csv")
    if err != nil {
        return err
    }
    defer f.Close() // 바로 스케줄됨!

    db, err := sql.Open("postgres", "...")
    if err != nil {
        // 여기서 자동으로 f.Close()가 실행됨
        return err
    }
    defer db.Close() // 바로 스케줄됨!

    // ... 40 lines of logic ...

    return nil
    // db.Close()가 여기서 자동으로 실행됨
    // f.Close()가 여기서 자동으로 실행됨
}

defer 키워드는 함수 호출을 스택에 넣는 거야,” Eleanor가 말했다. “즉, ‘함수가 어떻게 끝나든—return이든, error이든, panic이든—떠나기 직전에 이 코드를 실행해.’ 라는 뜻이야.”

“그럼 정리 코드를 생성 바로 옆에 두는 거구나?”

“항상 그래. 문을 열면 바로 문에게 떠날 때 스스로 닫으라고 알려줘. 다시는 잊지 않을 거야. 파일, 데이터베이스 연결, 특히 뮤텍스 락에도 동일하게 적용돼.”

mu.Lock()
defer mu.Unlock() // 무슨 일이 있어도 락이 해제됨

“잠깐,” Ethan이 코드를 보며 말했다. “이제 defer가 두 개야. 어느 것이 먼저 실행돼?”

“스택이니까,” Eleanor가 답했다. “Last In, First Out(후입선출).”

그녀는 메모장에 스케치를 그렸다:

  1. 파일 열기 → f.Close 푸시
  2. DB 열기 → db.Close 푸시
  3. 함수 종료 → db.Close 팝 (먼저 실행) → f.Close 팝 (두 번째 실행)

“이게 중요해,” 그녀가 강조했다. “버퍼드 라이터를 만든다고 생각해 보자. 파일을 Close 하기 전에 버퍼를 Flush 해야 해. 파일을 먼저 만들고 라이터를 두 번째에 만들었으니, 라이터가 먼저 닫히게 돼. 의존 순서가 자연스럽게 처리되는 거지.”

Argument Evaluation Gotcha

“한 가지 주의점,” Eleanor가 손가락을 들어올리며 덧붙였다. “함수 호출은 나중에 실행되도록 예약되지만, 인자는 지금 바로 평가돼.”

“그게 무슨 뜻이야?”

“이걸 봐.”

func TrackTime() {
    start := time.Now()
    defer fmt.Println("Time elapsed:", time.Since(start))

    time.Sleep(2 * time.Second)
}

Ethan이 코드를 실행했다.

Time elapsed: 0s

“0이야?” Ethan이 물었다. “하지만 2초 동안 잠들었잖아.”

time.Since(start)defer 라인을 쓸 때 이미 계산됐기 때문이야.” Eleanor가 설명했다. “그 순간 start는 현재 시각이었으니 차이는 0이었어.”

Fixing the Timing Example

// (예시를 고치는 코드가 여기서 이어집니다)
o
func TrackTime() {
    start := time.Now()
    defer func() {
        fmt.Println("Time elapsed:", time.Since(start))
    }()

    time.Sleep(2 * time.Second)
}

다시 실행하면 다음과 같이 출력됩니다:

Time elapsed: 2s

이제 계산이 익명 함수 내부, 즉 가장 마지막에 이루어집니다.

Ethan은 자신의 ExportData 함수를 바라보았습니다. 훨씬 안전해 보이고 견고했습니다.

“예전에는 defer가 단순히 문법 설탕일 뿐이라고 생각했어요,” 그가 말했습니다.

“그건 안전망이에요,” Eleanor가 바로잡았습니다. “우리는 인간이니까요. 우리는 일을 잊어버리고, 산만해지죠. defer는 진행하면서 정리할 수 있게 해 주어, 미래의 자신에게 혼란을 남기지 않게 해 줍니다.”

defer

사용 사례

  • 파일 닫기 (f.Close())
  • 뮤텍스 잠금 해제 (mu.Unlock())
  • 데이터베이스 연결 닫기 (db.Close())

실행 순서 (LIFO)

  • 지연 호출은 스택에 저장됩니다.
  • 주변 함수가 반환될 때 마지막으로 지연된 함수가 먼저 실행됩니다.

패닉 안전성

  • 함수가 패닉이 발생해도 지연 함수가 실행되어 신뢰할 수 있는 정리 경로를 제공합니다.

인자 평가

  • defer 문에 도달했을 때 지연 함수의 인자는 즉시 평가됩니다.
  • 함수 본체는 나중에, 반환 시점에 실행됩니다.

함수 끝에서 무언가를 계산해야 할 경우(예: 시간 측정), 로직을 익명 함수로 감싸세요:

defer func() {
    // computation that should happen just before the function exits
}()

다음 장: Panic과 Recover. Ethan은 때때로 프로그램을 구하는 유일한 방법은 프로그램을 강제로 크래시시키고(그 후에 잡는) 것이라는 것을 배웁니다.

Back to Blog

관련 글

더 보기 »

Zig vs Go: 제네릭

소개 Go는 버전 1.18에서 generics를 도입하여 함수와 struct를 타입으로 매개변수화할 수 있게 했습니다. Zig는 오랫동안 compile‑time generics를 지원해 왔습니다.