본문 바로가기

프로그래밍/golang

[Golang] Close() 에러를 무시하면 안 되는 이유

 

Go에서 자원을 해제할 때 사용하는 "defer Close()" 는 매우 편리하지만, 반환되는 에러를 무시하는 경우가 많이 있습니다.

그런데 특정 상황에서는 Close() 에러를 무시하는 것이 큰 문제로 발전할 수도 있습니다.

그 이유와 해결 방법을 함께 알아봅시다.

 

1. Close()

"Close()"는 단순한 자원 반납이 아니라, 메모리에 머물던 데이터를 확정하는 마지막 단계입니다.

예시를 살펴보면 이해가 더 잘 될 것 입니다.

  • gzip.Writer : Close() 호출 시 압축 스트림의 마지막 트레일러와 CRC 체크섬 등 마무리 바이트를 기록합니다.
    만약 이때 실패하면 파일은 불완전한 상태가 됩니다.
  • bufio.Writer : 버퍼에 남은 데이터들을 실제 파일이나 네트워크로 밀어내는 Flush 작업이 Close() 시점에 진행됩니다.

이는 Close() 의 실패가 전체 실패로 이어질 수 있다는 것을 알 수 있습니다.

 

2. 왜 그러면 지금까지 Close 에러를 무시했나

Go의 에러 핸들링 패턴에서는 두 가지 에러가 동시에 발생 했을 때 하나를 포기해야하는 구조적인 한계가 있었습니다.

두 경우은 다음과 같습니다.

 

  • 로직에서 에러가 났는데, defer 내에서 Close에서도 에러가 발생한다면,
    마지막 실행된 Close 에러가 원본 에러를 덮어버려 진짜 에러의 원인을 찾지 못함
  • 로직에서 발생한 에러를 유지하기 위해서 defer Close()처럼 반환 값을 무시 

위 두 경우처럼 덮어쓰기가 되거나, 아니면 무시하는 방식으로 에러를 핸들링 했습니다.
즉 Origin Error 를 볼 것인가, 아니면 Close Error 를 볼 것인가에서 보통은 Close Error를 포기했습니다.

 

3. errors.join

"Go 1.20+" 부터 도입된 "errors.join"은 이러한 문제들을 해결하게 됩니다.

 

- 예제

package main

import (
	"compress/gzip"
	"errors"
	"fmt"
	"os"
)

func writeGzipFile(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}

	gz := gzip.NewWriter(f)

	defer func() {
		err = errors.Join(err, gz.Close(), f.Close())
	}()

	if _, err := gz.Write(data); err != nil {
		return fmt.Errorf("gzip write: %w", err)
	}

	return nil
}

func main() {
	fmt.Println(writeGzipFile("out.txt.gz", []byte("hello\n")))
}

 

위 예시를 살펴 보고 데이터의 무결성이 중요한 로직의 경우는 errors.join을 활용해서 에러를 핸들링 하면 되겠습니다.

 

4. 전체 코드 보기

https://github.com/reochoi109/go-handbook/blob/main/error/advanced/multierror/main.go

 

go-handbook/error/advanced/multierror/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com