23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
안녕하세요, 데보션영 3기 강대훈입니다.
본 포스팅은 golang을 처음 공부할 즈음 error 처리 방식에 대해 많은 고민을 했고,
당시 많은 도움을 받았던 Don’t just check errors, handle them gracefully 라는 아티클을 기반으로 작성하였습니다.
프로그램은 여러 가지 원인으로 인해서 의도하지 않은 방향으로 동작하거나, 종료될 수 있습니다.
따라서 어떤 언어를 선택하든간에 로깅과 에러핸들링, 테스팅은 가장 기본적이며 가장 먼저 체화해야 한다고 생각합니다.
다양한 언어에서 오류/예외를 처리할 때 Opaque(불투명) error handling을 채택하곤 합니다.
그렇다면 golang에서는 왜 Opaque error handling을 선택해야 하며, Opaque erro handling이란 무엇인가 알아보겠습니다.
불투명한 반환 타입이 있는 함수 또는 메서드는 반환값의 타입 정보를 가립니다.
함수의 반환 타입으로 구체적인 타입을 제공하는 대신에 반환값은 지원되는 프로토콜 측면에서 설명됩니다.
반환값의 기본 타입이 비공개로 유지될 수 있으므로 모듈과 모듈을 호출하는 코드 사이의 경계에서 타입 정보를 숨기는 것이 유용합니다.
타입이 프로토콜 타입인 값을 반환하는 것과 달리 불투명한 타입은 타입 정체성을 보존합니다. ➡️컴파일러는 타입 정보에 접근할 수 있지만 모듈의 클라이언트는 그럴 수 없습니다.
제네릭과는 반대되는 개념이라고 생각할 수 있습니다.
알고리즘 속에서 종료 조건을 나타내거나, 더 이상 처리가 불가능한 상황을 나타내는 값을 sentinel value라고 합니다. 예를 들면 다음 코드의 -1은 sentinel value 입니다.
int find(int arr[], size_t len, int val)
{
for (int i = 0; i < len; i++)
if (arr[i] == val)
return i;
return -1; // not found
}
func exam(){
err := find(arr,arrLen,val);
if err == -1{
//에러처리
}
}sentinel errors로 error를 처리할 경우, 함수를 호출한 쪽에서 if error == 특정값 연산을 통해서 에러를 처리합니다.
이 "특정값"이 다양한 문제를 일으킵니다.
필연적으로 호출자는 error를 검사하기위해 "특정값"을 import해야합니다.
즉, package간에 의존성이 생깁니다.
또는 이 sentinel error를 처리하는 로직 속에 error를 반환하는 interface를 정의했다면, 그 interface의 구현체들은 모두 그 error 값만 을 반환하는 제약이 생깁니다.
error를 특정 구조체로 만들어, error가 추가적인 context 정보를 포함하게 하는 방식입니다.
type MyError struct {
Msg string
File string
Line int
}sentinel error와 다르게 일반적인 error에 다른 정보를 감싸서 error를 처리하는 모습입니다.
하지만 error type 방식도 여전히 sentinel error가 가졌던 문제점을 가지고 있습니다.
즉, error를 처리하는 모든 패키지는 정의된 MyError 패키지에 대한 의존성을 갖게 됩니다.
단지 error 를 검사만 하지 말고, 우아하게 처리하자.
사용하면서 정말 좋다는 것을 몸으로 체감한 에러 핸들링 방식입니다.
타입이 아니라 행위(behavior)에 대해 error assertion 을 하자.
어떤 경우에는 에러 처리에 대해 이분법적 접근만으로는 충분하지가 않습니다.
예를 들면, 만약 내 process 의 바깥 세상과의 상호 작용(network를 통한 동작같은)에서는, 호출자가 작업을 재수행하는 것이 좋은지를 판단하기 위해 error 를 조사해야할 경우가 있습니다.
(만약 error 발생 시 network 재연결을 원한다거나 등)
다음은 그 예시입니다.
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}IsTemporary 함수는 주어진 오류 err이 임시적인지 여부를 확인합니다.
err.(temporary) 는 err이 temporary 인터페이스를 구현하는 경우 해당 인터페이스 타입으로 변환합니다. 반환된 te 변수에는 Temporary() 메서드가 포함됩니다.
ok 변수는 타입 변환이 성공적으로 이루어졌는지 여부를 나타내는 부울 값입니다.
최종적으로 함수는 ok 가 true 이고, te.Temporary() 가 true 일 때에만 true 를 반환합니다.
이는 오류가 temporary 인터페이스를 구현하면서 동시에 Temporary() 메서드가 true 를 반환하는 경우에만 true 를 반환하도록 하는 것입니다.
즉, IsTemporary에 발생한 error를 넣었을 때 true가 반환된다면 이는 일시적 에러를 의미하며 추가적인 동작을 수행할 수 있게됩니다.
여기서 중요한 점은 error를 정의하는 패키지를 import 할 필요없이 구현될 수 있다는 것입니다.
또한 opaque error는 error가 발생하면 그대로 흘려보냅니다. 다만, error에 특정 Context를 추가하며 마치 stacktrace와 같은 느낌을 줄 수 있습니다.
go는 github.com/pkg/errors 패키지의 Wrap()과 Cause()함수로 Error에 Context를 추가하거나, 복원할 수 있습니다.
🤔예시
func getIPAddresses(url string) ([]string, error) {
// Using LookupHost to lookup IP addresses of domain
ips, err := net.LookupHost(url)
if err != nil {
//LookupHost 에서 발생한 err에 "LookupHost failed"라는 context가 추가되었다.
return nil, errors.Wrap(err, "LookupHost failed : ")
}
err:= invalidUrl(url)
if err != nil {
//invalidUrl 에서 발생한 err에 "Invalid Url"라는 context가 추가되었다.
return nil, errors.Wrap(err, "Invalid Url : ")
}
return ips, nil
}
func findAndSaveNameServerInfo(ns []string, outputDB *sql.DB, searchId int) error {
var nameserverIPs []string
var err error
for _, nameserver := range ns {
IPs, err := getIPAddresses(nameserver)
if err != nil {
// getIPAddresses에서 발생한 err에 네임서버 정보 context가 추가되었다.
errors.Wrap(err, "\n--->Error for Nameservers : "+nameserver+err.Error()+"\n")
continue
}
.
.
}
}
func main(){
.
.
err = findAndSaveNameServerInfo(ns, outputDB, url.URLId)
if err != nil {
logger.Warn("Problem with find and save NS : " + err.Error())
}
.
.
}opaque 에러를 구현한 위 예시를 보겠습니다.
만약 getIpAddresses에서 에러가 난다면, errors에는 어느 url에서 에러가 발생되었는지가 중첩되어 쌓이게 됩니다.
그리고 이후 ns에 대한 for문이 끝나게 되면, main함수는 에러가 발견된 네임서버리스트의 모든 네임서버에 대한 에러를 출력합니다.
따라서 해당 메시지를 통해서 어떤 위치에서 어떤 에러가 발생했는지 한 눈에 확인할 수 있습니다.
에러는 한번만 출력하자.
• 에러 로깅은 parent에게 맡기는게 원칙이고, 만약 자식 메서드에서 모두 에러를 출력한다면 에러 로그가 굉장히 중복되고 더러워질 것입니다.
다음은 실제 뱅크샐러드라는 기업에서 사용 중인 에러 컨벤션입니다.
// ❌
if err := nil {
log.Println("failed to get user id: %d", userID)
return err
}
// ✅
if err := nil {
return err
}오류 또한 public API의 일부이므로, public API의 다른 부분과 마찬가지로 신중히 작성해야 하며 의존성을 줄여야 합니다.
Sentinal error 값의 수를 최소화하고, errors.Wrap을 통해 error에 context 정보를 추가합니다.
유연성을 극대화하기 위해선 모든 오류를 opaue error로 처리하는 것이 좋습니다.
그렇게 할 수 없다면, error의 값이 아닌, 행위 구현자로 error를 검사하는 것이 좋습니다.
만약 도중에 error를 검사할 필요가 있다면, errors.Cause로 복원합니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.