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 를 되도록 사용하지 말라는 것에는 변함이 없습니다.