🔥 defer와 함수 호출 스택

343자
5분

Go 언어에서 defer 키워드를 사용하면 함수나 메서드의 실행을 지연시킬 수 있어요. 지연된 함수 호출은 스택(stack)에 쌓이게 되죠. 그리고 현재 함수가 리턴할 때, 스택에 쌓인 지연 호출들이 LIFO(Last-In-First-Out) 순서로 실행된답니다.

defer에 대해 더 자세히 알고 싶다면 이 블로그 포스트를 읽어보세요.

자, 그럼 예제 코드를 통해 defer가 어떻게 동작하는지 알아볼까요?

package main
 
import "fmt"
 
func main() {
	fmt.Println("카운팅 시작") // 가장 먼저 출력됩니다.
 
	for i := 0; i < 10; i++ {
		defer fmt.Println(i) // 루프를 돌면서 지연 호출을 스택에 쌓습니다.
	}
 
	fmt.Println("끝!") // 카운팅이 끝난 후 출력됩니다.
}
 
go

위 코드를 실행하면 다음과 같은 출력 결과를 얻을 수 있어요.

카운팅 시작
끝!
9
8
7
6
5
4
3
2
1
0
text

코드의 실행 흐름을 단계별로 살펴보면:

  1. main() 함수가 실행되면서 가장 먼저 "카운팅 시작"이 출력돼요.
  2. 그 다음, for 루프가 시작되죠. 루프 변수 i는 0부터 9까지 증가하면서 반복합니다.
  3. 루프 내에서 defer fmt.Println(i)가 호출되는데, 이는 해당 출력문의 실행을 지연시키고 스택에 쌓아둡니다. 즉, 루프를 돌면서 defer로 지정된 출력문들이 차례로 스택에 쌓이게 되는 거예요.
  4. 루프가 끝나면 "끝!"이 출력됩니다. 아직 defer로 지정된 출력문들은 실행되지 않은 상태죠.
  5. main() 함수가 리턴하기 직전, 스택에 쌓여있던 지연 호출들이 LIFO 순서로 실행됩니다. 가장 마지막에 스택에 쌓인 defer fmt.Println(9)가 가장 먼저 실행되고, 그 다음은 defer fmt.Println(8), ... 이런 식으로 역순으로 실행되어 9부터 0까지 차례로 출력되는 거죠.

이렇게 defer를 사용하면 현재 함수가 리턴하기 직전에 실행되어야 할 작업들을 깔끔하게 정리할 수 있답니다. 자원 해제, 클린업 작업 등에 유용하게 사용할 수 있어요.

defer의 또 다른 장점은 에러 처리에 있어요. 에러가 발생했을 때 defer로 지정된 코드는 항상 실행이 보장되거든요. 이를 이용해 에러 발생 시 자원 해제나 롤백 등의 작업을 안전하게 수행할 수 있습니다.

func doSomething() error {
	file, err := os.Open("file.txt") // 파일을 엽니다.
	if err != nil {
		return err
	}
	defer file.Close() // 함수 종료 직전 파일을 닫습니다. 에러 발생해도 실행 보장!
 
	// 파일 처리 작업 수행...
	// ...
 
	return nil
}
 
go

이런 식으로 에러가 발생하더라도 defer로 지정된 file.Close()는 반드시 실행되므로, 열어둔 파일 핸들이 제대로 닫힘을 보장할 수 있는 거예요.

지금까지 defer에 대해 알아봤는데요. 함수 리턴 직전에 실행되어야 할 작업이 있다면 defer를 적극 활용해 보세요.