1. 고랭에서 defer 기능 소개

고(Go) 언어에서 defer 문은 defer 문을 포함하는 함수가 실행을 마칠 때까지 그 뒤에 오는 함수 호출의 실행을 지연시킵니다. 이것은 다른 프로그래밍 언어의 finally 블록과 유사하지만, defer의 사용법은 더 유연하고 독특합니다.

defer를 사용하는 이점은 파일을 닫거나 뮤텍스(상호배제) 잠금을 해제하거나 함수의 종료 시간을 기록하는 등의 정리 작업에 사용할 수 있다는 것입니다. 이를 통해 프로그램을 더 견고하게 만들고 예외 처리에 필요한 프로그래밍 작업을 줄일 수 있습니다. 고의 설계 철학에서는 오류 처리, 자원 정리 및 기타 후속 작업을 처리할 때 defer의 사용을 권장하고 있습니다. 왜냐하면 이는 코드를 간결하고 가독성 있게 유지하는 데 도움이 되기 때문입니다.

2. defer의 작동 원리

2.1 기본 작동 원리

defer의 기본 작동 원리는 스택(LIFO, Last In, First Out)을 사용하여 각 지연된 함수를 저장하여 실행될 것을 보류하는 것입니다. defer 문이 나타나면 고(Go) 언어는 즉시 문 뒤에 오는 함수를 실행하지 않습니다. 대신에 해당 함수를 전용 스택에 넣습니다.이러한 지연된 함수들은 외부 함수가 반환될 때에만 스택의 순서대로 실행되며, 마지막으로 선언된 defer 문의 함수가 먼저 실행됩니다.

덧붙여, defer 문 뒤에 오는 함수의 매개변수는 실제 실행 시가 아니라 defer가 선언된 순간에 계산되고 결정됨에 유의해야 합니다.

func example() {
    defer fmt.Println("world") // 지연됨
    fmt.Println("hello")
}

func main() {
    example()
}

위의 코드는 다음과 같은 결과를 출력할 것입니다:

hello
world

worldhello보다 코드 상에서 앞에 나오지만, 실제로는 example 함수가 종료되기 전에 출력됩니다.

2.2 여러 defer 문의 실행 순서

함수에 여러 defer 문이 있는 경우, 이들은 후입선출(LIFO, Last In, First Out) 순서로 실행됩니다. 이는 복잡한 정리 로직을 이해하는 데 매우 중요합니다. 다음 예제는 여러 defer 문의 실행 순서를 보여줍니다:

func multipleDefers() {
    defer fmt.Println("첫 번째 defer")
    defer fmt.Println("두 번째 defer")
    defer fmt.Println("세 번째 defer")

    fmt.Println("함수 본문")
}

func main() {
    multipleDefers()
}

이 코드의 출력은 다음과 같을 것입니다:

함수 본문
세 번째 defer
두 번째 defer
첫 번째 defer

defer는 후입선출(LIFO) 원리를 따르므로 "첫 번째 defer"가 가장 먼저 선언되었지만 가장 나중에 실행됩니다.

3. 다양한 시나리오에서 defer의 활용

3.1 자원 해제

고(Go) 언어에서 defer 문은 파일 조작 및 데이터베이스 연결과 같은 자원 해제 로직을 처리하는 데 일반적으로 사용됩니다. defer를 사용하면 함수를 떠나는 이유와 관계없이 해당 자원이 올바르게 해제되도록 보장할 수 있습니다.

파일 조작 예제:

func 파일읽기(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    // 파일이 올바르게 닫히도록 defer를 사용
    defer file.Close()

    // 파일 읽기 작업 수행...
}

이 예제에서 os.Open이 파일을 성공적으로 열면 이후의 defer file.Close() 문은 함수를 떠날 때 파일 자원이 올바르게 닫히고 파일 핸들 자원이 해제될 것입니다.

데이터베이스 연결 예제:

func 데이터베이스쿼리(query string) {
    db, err := sql.Open("mysql", "사용자:비밀번호@/데이터베이스명")
    if err != nil {
        log.Fatal(err)
    }
    // defer를 사용하여 데이터베이스 연결이 닫히도록 보장
    defer db.Close()

    // 데이터베이스 쿼리 작업 수행...
}

마찬가지로 defer db.Close()데이터베이스쿼리 함수를 떠나는 경우에 데이터베이스 연결이 닫히도록 보장합니다 (정상적인 반환 또는 예외 발생).

3.2 동시 프로그래밍에서의 락(Lock) 작업

동시 프로그래밍에서 defer를 사용하여 뮤텍스 잠금 해제를 처리하는 것은 좋은 실천법입니다. 이를 통해 임계 영역 코드 실행 후에 락이 올바르게 해제되므로 교착 상태를 피할 수 있습니다.

뮤텍스(Mutex) 락 예시:

var mutex sync.Mutex

func updateSharedResource() {
    mutex.Lock()
    // 락이 해제되도록 하기 위해 defer를 사용합니다
    defer mutex.Unlock()

    // 공유 리소스를 수정합니다...
}

공유 리소스의 수정이 성공적인 경우든, 패닉이 발생한 경우든, deferUnlock()이 호출되어 다른 고루틴이 해당 락을 얻을 수 있도록 합니다.

팁: 뮤텍스 락에 대한 자세한 설명은 이어지는 장에서 다룰 예정입니다. 현재 단계에서는 defer의 응용 시나리오를 이해하는 것이 충분합니다.

defer를 위한 3가지 흔한 실수와 고려 사항

defer를 사용하면 코드의 가독성과 유지보수성이 크게 향상되지만, 몇 가지 주의해야 할 실수와 고려할 사항도 있습니다.

3.1 defer 함수의 매개변수는 즉시 평가됩니다

func printValue(v int) {
    fmt.Println("값:", v)
}

func main() {
    value := 1
    defer printValue(value)
    // `defer`로 전달된 매개변수에 영향을 주지 않습니다.
    value = 2
}
// 출력 결과는 "값: 1"입니다

defer 문 이후에 value의 값이 변경되었더라도, defer에서 전달된 매개변수는 이미 평가되고 고정되어 있기 때문에 출력 결과는 여전히 "값: 1"입니다.

3.2 반복문 안에서 defer 사용 시 주의해야 합니다

반복문 안에서 defer를 사용하면 루프가 종료되기 전에 리소스가 해제되지 않을 수 있으며, 이는 리소스 누출이나 고갈로 이어질 수 있습니다.

3.3 "사용 후 해제"를 동시성 프로그래밍에서 피하세요

동시성 프로그램에서 defer를 사용하여 리소스를 해제할 때, 리소스가 해제된 후에 모든 고루틴이 해당 리소스에 액세스하지 않도록하여 경합 조건을 방지해야 합니다.

4. defer 문의 실행 순서에 주의하세요

defer 문은 후입선출(LIFO) 원칙을 따르며, 가장 마지막에 선언된 defer가 먼저 실행됩니다.

해결책과 모범 사례:

  • defer 문의 함수 매개변수는 선언 시점에 평가된다는 사실을 항상 유의하세요.
  • 반복문 안에서 defer를 사용할 때는 익명 함수를 사용하거나 리소스 해제를 명시적으로 호출하는 것을 고려하세요.
  • 동시성 환경에서 defer를 사용하여 리소스를 해제하기 전에 모든 고루틴이 작업을 마쳤는지 확인하세요.
  • 여러 defer 문이 포함된 함수를 작성할 때는 실행 순서와 로직을 신중하게 고려하세요.

이러한 모범 사례를 따르면 defer를 사용할 때 발생하는 대부분의 문제를 피하고 더 견고하고 유지보수성 있는 Go 코드를 작성할 수 있습니다.