JWK 라이브러리를 만들어 보자! #8.5

김성현·2022년 1월 5일
0
post-thumbnail

8.5편 : ... 에러를 발견했다.

에러를 발견했다.

단위 테스팅 중이였다.

문제가 된 부분은 아래 코드이다.

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를 리턴했다.

그래서 이제부터 예상가는 시나리오들을 찾아 문제를 해결해야 한다.

시나리오 1 : struct 안의 string 비교

FieldError는 함수고 이 함수는 *fieldErrorerror 인터페이스 형식으로 리턴한다.

type fieldError struct{
    field string
}

fieldError의 정의는 위와 같은데 아마 fieldErrorfieldError.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도 잘 비교해 줬던것 같은 기억이 나서 해당 부분은 문제가 되지 않을거라는 생각이 들었다.

해당 시나리오는 폐기다.

시나리오 2 : errors.Is 의 동작 특징에 따른 이유

디버거를 통해 값을 추적하던 중 의심스런 부분을 바로 발견했다.

좌측의 디버거 탐색기를 보면 err하고 target은 모두 같은 타입이지만, jwk.fieldError가 아닌 *jwk.fieldError 타입이였다.

생각해 보니 이런 상태라면 이녀석은 구조체 단위의 equal이 아닌 포인터의 equal작업을 수행할 거라는 의심이 들었다.

그래서 jwk.fieldErrorfunc(e *fieldError) Is(error) bool 함수를 추가로 구현해 줬다.

만약 그렇다면 첫번째 isComparable && err == target 부분에서 false가 나더라도 아래 Is 인터페이스와 메서드를 통한 체크에서 true를 통해 성공적으로 리턴될 것이라고 생각했기 때문이다.

이제 잘 수 있다.

profile
수준 높은 기술 포스트를 위해서 노력중...

0개의 댓글