Constant errors

이 글은 constant error 에 대한 Dave Cheney 의 블로그 글을 번역한 것입니다.
https://dave.cheney.net/2016/04/07/constant-errors

저는 이 블로그 내용을 현재 Go 패키지에 정의된(ex> io/io.go) Error 값들(sentinel error 값들)이 상수가 아니기 때문에 발생할 수 있는 문제점들을 지적한 글로 이해했습니다.
예를 들면, 아래의 Error 값들이 const 로 선언된게 아니고 그냥 일반 변수로 선언되었는데, 이럴 때 어떤 문제점이 있을 수 있는지를 고민해본 글인거죠.

var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")

(역자: 이 글을 읽기 전에 sentinel 이라는 단어가 갖는 의미를 먼저 읽어보세요)

이 글은 Go 에서의 sentinel error 값(파수꾼 에러값)들 (번역글: "에러는 검사만 하지 말고, 우아하게 처리하세요")에 대한 저의 생각을 한번 실험해 본 것입니다.

Sentinel error 는 좋지 않아요.
왜냐 하면, 소스 코드와 runtime 의 강결합을 유발하기 때문인데, 그래도 때로는 sentinel error 가 필요할 때도 있긴 합니다.
io.EOF 는 이러한 sentinel value 중의 하나인데요.
이상적으로는 sentinel value 는 상수처럼 동작해야 합니다.
값은 불변해야하고 쉽게 대체 가능해야 하죠.

제 생각에 첫번째로 문제가 되는 것은 io.EOF 가 public 변수라는 점입니다.
즉, 패키지 외부로 open 되는 변수라는거죠.

현재 io 패키지를 import 하는 어떤 코드라도 io.EOF 의 값을 변경할 수 있는데요.
대부분의 경우 이는 문제가 되지 않지만, 때로는 debugging 을 굉장히 어렵게 만들 수가 있습니다.
아래 코드를 보세요.
io.EOF 의 값을 변경할 수 있을 때 어떤 상황이 발생할 수 있는지...

fmt.Println(io.EOF == io.EOF) // true
x := io.EOF
fmt.Println(io.EOF == x)      // true
	
io.EOF = fmt.Errorf("whoops")
fmt.Println(io.EOF == io.EOF) // true
fmt.Println(x == io.EOF)      // false

두번째 문제점은 io.EOF 가 마치 상수가 아니라 singleton 처럼 동작한다는 거예요.

우리가 우리 자신의 EOF value 를 생성할 때 io 패키지의 방법을 똑같이 따라하더라도, 우리가 생성한 값과 io.EOF 를 서로 비교할 수가 없다는 문제가 있습니다.
아래 코드를 보세요.
우리는 io/io.go 에서 생성할 때 사용한 방법으로 똑같이 우리만의 error 값을 생성했는데, 이를 io.EOF 와 제대로 비교할 수가 없잖아요.
예상과 다른 결과가 나오죠.

package main

import (
    "errors"
    "fmt"
    "io"
)

func main() {
    err := errors.New("EOF")   // io/io.go line 38
    fmt.Println(io.EOF == err) // false
}

이처럼 errors.New 또는 fmt.Errorf 로 만든 sentinel error 값들은 상수가 아니라는 점 때문에 여러분은 이상한 현상들을 만나게 됩니다.

Constant errors

저의 해답을 내놓기 전에, Go 에서 error interface 가 어떻게 동작하는지를 한번 되돌아 봅시다.
아시다시피 Error() string 메소드를 가진 타입은 모두 error interface 를 구현(준수)한 것입니다. 상수 string 뿐 아니라 string 같은 원시(primitive) 타입의 경우도 마찬가지입니다.

이러한 사실을 바탕으로 error 구현에 대해 한번 생각해보죠.

type Error string

func (e Error) Error() string { return string(e) }

위의 Error 타입은 error interface 를 구현한 것이므로errors.errorString 구현과 유사해 보입니다.
하지만, errors.errorString 과는 달리 Error 타입은 아래처럼 const 구문에서 사용될 수 있어요.
errors.errorString 은 아래처럼 const 와 함께 사용되면 빌드 시 에러가 발생합니다.
const 초기화 구문에서 사용할 수가 없어요.

const err = Error("EOF") 
const err2 = errorString{"EOF"} // const initializer errorString literal is not a constant

아래처럼 상수화된 Error 타입의 경우는 변수가 아니기 때문에 당연히 값을 변경할 수 없습니다.

const err = Error("EOF") 
err = Error("not EOF") // error, cannot assign to err

두 개의 상수 string 은 내용이 같다면 언제나 equal 을 만족하죠.
즉, 같은 내용물을 가진 두 개의 Error 값들은 equal 을 만족한다는 얘기입니다.
아래처럼요. 전혀 문제없죠.

const err = Error("EOF") 
fmt.Println(err == Error("EOF")) // true

equal 을 만족하는 Error 값들은 서로 정확히 같은 것이잖아요.
마치 상수 1 이 다른 어떤 상수 1 과도 똑같은 것인 것처럼요.

const eof = Error("eof")

type Reader struct{}

func (r *Reader) Read([]byte) (int, error) {
        return 0, eof
}

func main() {
        var r Reader
        _, err := r.Read([]byte{})
        fmt.Println(err == eof) // true
}

io 패키지에 있는 io.EOF 의 정의를 상수로 변경할 수는 없을까요?
아래 코드처럼요.

-var EOF = errors.New("EOF")
+const EOF = errors.Error("EOF")

이렇게 변경하고 compile 을 해보면 문제없어 보이고, 모든 test 들도 통과하긴 하네요.
하지만, 이러한 변경은 Go 의 첫번째 계약(contract)에 대한 확장이 필요하겠네요.
(역자: 이미 확정된 외부와의 인터페이기 때문에 변경이 힘들 것 같다는 의미인 듯...)

하지만, 어쨌건 여러분의 코드에서 sentinel error 를 되도록 사용하지 말라는 것에는 변함이 없습니다.

You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x