에러를 발견했다.
단위 테스팅 중이였다.
문제가 된 부분은 아래 코드이다.
package jwk_test
...
//go:embed embeding/rsa-pub-without-n.json
var rsaPubWithoutN string
func TestDecode(t *testing.T) {
...
t.Run("without 'n' RSA public key", func(t *testing.T) {
_, err := jwk.DecodeKey(strings.NewReader(rsaPubWithoutN))
if err == nil {
t.Fatalf("expected value not <nil>, but got <nil>")
}
if !errors.Is(err, jwk.ErrRequirement) {
t.Fatalf("expected error is %v, but not", jwk.ErrRequirement)
}
if !errors.Is(err, jwk.FieldError("n")) {
t.Fatalf("expected %v is %v, but not", err, jwk.FieldError("n"))
}
})
}
여기서 문제가 된 부분은 errors.Is(err, jwk.FieldError("n"))
이 부분이다.
어째서인지 예상과 다르게 해당 부분은 true
를 리턴하지 못했다.
참고로 rsaPubWithoutN
은 JWK Key를 만족하지만 RSA 공개키 형식임에도 n
필드가 존재하지 않는 타입이다.
이유는 간단했다. 내가 errors.Unwrap
을 거꾸로 구현했다.
지금은 wrapError
타입이 wrapError.parent
하고 wrapError.value
두 값을 가지고 있다.
그런데 만약 이런 값이 들어온다 생각해 보자
a := makeError(Err1, Err2)
b := makeError(Err0, a)
이 경우 구조체를 표현하면
{
parent : Err0,
value : {
parent : Err1
value : Err2
}
}
이런 식의 값이 되어버린다.
내가 원한 형태는 이런 형태인데
{
parent : {
parent : Err0
value : Err1
},
value : Err2
}
하지만 코드를 약간 수정하려 했더니 errors.Unwrap
은 오로지 부모를 얻을수만 있고 수정할수는 없었다.
결국, 해당부분은 내가 처음부터 parent
가 아닌 child
를 저장하는 형태로 만들었어야 했다.
사실 문제 해결법은 간단하고, 그냥 새벽 3시까지 졸린 상태에서 코딩하다 있을 법한 멍청한 실수였다.
그래서 오늘은 지난날의 내가 새벽 3시까지 졸린 상태로 코딩하다 한 실수를 고치기 위해 새벽 3시에 수정중이다.
분명 효율이 별로기는 한데 집중하다 보면 피곤해도 자꾸 코딩하다 이런 실수를 저지른다.
다행이도 해당 타입은 특정 함수를 통해서만 사용하는 구조였으므로 해당 함수와 구조체를 약간만 손보면 해결이 가능하다.
그럼 이제 이거만 수정하고 잠들면 될 것 같다.
다들 좋은 밤 되세요~
...라고 할뻔
참고로 이건 진짜로 실시간으로 고치면서 쓰는 글이다.
진짜로 지금은 3시반이고 예상치 못한 동작에 고통받는 중이다.
위의 구현을 모두 수정하고 난 뒤에 일반 에러들은 잘 감지하는데 특정 필드의 에러를 감지할 때는 예상치 못한 동작이 나는 것을 확인했다.
그 이유는 아래와 같다.
나는 특정 필드에서 난 에러를 감지하기 위해 FieldError
라는 개념을 만들었다.
만약 n
필드에서 난 에러를 감지할 때는
if errors.Is(err, FieldError("n")){...}
이런 식으로 감지할 수 있게 만드려고 했다.
그런데 디버거를 통해 보다보니 이상한 점을 발견했다.
그래서 설마설마 하고 확인하다 보니... 아니나 다를까 문제를 발견했다.
errors.Is(FieldError("n"), FieldError("n"))
은 반드시 true
여야 한다. 설명할 필요도 없이 당연하다.
그런데 이 값이 false
를 리턴했다.
그래서 이제부터 예상가는 시나리오들을 찾아 문제를 해결해야 한다.
FieldError
는 함수고 이 함수는 *fieldError
를 error
인터페이스 형식으로 리턴한다.
type fieldError struct{
field string
}
fieldError
의 정의는 위와 같은데 아마 fieldError
의 fieldError.field
를 비교할 때 go에서 string
은 당연히 comparable
이라(우리가 생각하는 strcmp
) 당연히 구조체로 래핑된 필드도 comparable
이라고 생각했는데 아닐수도 있다는 생각이 들었다.
대부분의 현대적 언어에서 이런 문제가 일어나는 건 본 적이 없지만 혹시나 싶었다.
그래서 만약 혹시 구조체 안의 string 필드는 strcmp
가 아니라 pointer eq
인지 확인하기 위해 아래 코드를 테스트해 보았다.
var n = "n"
errors.Is(jwk.FieldError(n), jwk.FieldError(n))
그런데 여전히 값은 true
가 아니라 false
였다.
아무리 생각해도 go는 구조체 내부의 string도 잘 비교해 줬던것 같은 기억이 나서 해당 부분은 문제가 되지 않을거라는 생각이 들었다.
해당 시나리오는 폐기다.
디버거를 통해 값을 추적하던 중 의심스런 부분을 바로 발견했다.
좌측의 디버거 탐색기를 보면 err
하고 target
은 모두 같은 타입이지만, jwk.fieldError
가 아닌 *jwk.fieldError
타입이였다.
생각해 보니 이런 상태라면 이녀석은 구조체 단위의 equal
이 아닌 포인터의 equal
작업을 수행할 거라는 의심이 들었다.
그래서 jwk.fieldError
에 func(e *fieldError) Is(error) bool
함수를 추가로 구현해 줬다.
만약 그렇다면 첫번째 isComparable && err == target
부분에서 false
가 나더라도 아래 Is
인터페이스와 메서드를 통한 체크에서 true
를 통해 성공적으로 리턴될 것이라고 생각했기 때문이다.
이제 잘 수 있다.