새소식

자바

Mockito에서만난에러때문에제네릭이해가쏙쏙되잖아

  • -

 

안녕하세요.

 

문제는 그렇게 시작됐습니다.

 

드디어 Mockito를 완전히 이해해 버린 도등어, 테스트 작성할 때 애매하다 싶으면 바로 Mockist가 되어버리고 있습니다.

 

오늘의 테스트 중인 코드는

@Component
public class AuthUserResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Login.class)
                && parameter.getParameterType().equals(AuthUser.class);
    }
    
    ...

 

인데요, 이거 상당히 문제 있습니다.

 

그니까 그 파라미터에 어노테이션 붙히면 자동으로 현재 로그인한 멤버 가져와 주는 리졸버 만드는 중이었는데

    @DisplayName("@Login 어노테이션이 있고 타입이 AuthUser가 아니면 false를 반환한다")
    @Test
    void supportsParameterWithoutTypeAuthUser() {
        // given
        MethodParameter parameter = Mockito.mock(MethodParameter.class);

        given(parameter.hasParameterAnnotation(Login.class))
                .willReturn(true);
        given(parameter.getParameterType())
                .willReturn(Object.class);

        // when
        boolean result = authUserResolver.supportsParameter(parameter);

        // then
        assertThat(result).isFalse();
    }

 

Login 어노테이션은 붙어있는데, 그걸 받는 타입이 AuthUser가 아니면 false를 반환하게 스터빙해서 false를 유도하는 그런 코드였습니다.

 

근데 이거

 

에러 납니다.

 

자 그래서 에러 메시지 잘 읽어보면

 

어... 스터빙 할 때...클래스...어..뭐...적용 불가능하다...Object 클래스 타입을

 

뭔 소린지 잘 모르겠고요

public Class<?> getParameterType() {
    Class<?> paramType = this.parameterType;
    if (paramType != null) {
        return paramType;
    }
    if (getContainingClass() != getDeclaringClass()) {
        paramType = ResolvableType.forMethodParameter(this, null, 1).resolve();
    }
    if (paramType == null) {
        paramType = computeParameterType();
    }
    this.parameterType = paramType;
    return paramType;
}

 

스터빙 하려는 메서드가 실제로는 이렇게 생긴걸 보니 일단 제네릭과 관련된 문제 같은데, 먼저 제네릭의 실행 과정에 대해 알아보겠습니다.

 

제네릭

제네릭은 예에엣날에 자바 5 버전 때 나온 기능으로, 컴파일 시점에 어떤 데이터 타입을 사용할지 미리 지정한다고 보면 됩니다.

 

예를 들어 List와 같은 자료구조에서 Integer만 받고 싶을 수 있는데, String 같은 게 들어오면 런타임 시점이 되어서야 알 수 있습니다.

 

그쵸?

 

아니 그래서 제네릭을 쓰면

 

이렇게 컴파일 시점에 확인을 띡 할 수 있습니다.

 

근데 이런 제네릭은 특이한 특징이 있습니다.

바로 하위 호환성을 위해 이렇게 굳이 설정해 놓은 타입을 컴파일 시점에는 없애버린다는 건데요

한번 볼까요?

@Test
void test() {
    // 예시 1: 컴파일 타임 vs 런타임
    List<String> stringList = new ArrayList<>();
    List<Integer> intList = new ArrayList<>();

    System.out.println("=== 제네릭 타입 소거 확인 ===");
    System.out.println("stringList 컴파일 타입: List<String> ✅");
    System.out.println("stringList 런타임 타입: " + stringList.getClass());
    // 출력: class java.util.ArrayList (String 정보 없음!)

    System.out.println("\nintList 컴파일 타입: List<Integer> ✅");
    System.out.println("intList 런타임 타입: " + intList.getClass());
    // 출력: class java.util.ArrayList (Integer 정보 없음!)

    System.out.println("\n두 타입이 같은가? " + (stringList.getClass() == intList.getClass()));
    // 출력: true (런타임에는 구분 안 됨!)
}

 

놀랍게도 런타임 시점에는 다른 두 타입이(List<String>, List<Integer>) 컴파일 시점에는 두 타입이 같다고 나와버립니다.

 

그럼 오류는 왜 난겨?

자 그럼 이제 다시 오류로 넘어와서

public Class<?> getParameterType() {
    Class<?> paramType = this.parameterType;
    if (paramType != null) {
        return paramType;
    }
    if (getContainingClass() != getDeclaringClass()) {
        paramType = ResolvableType.forMethodParameter(this, null, 1).resolve();
    }
    if (paramType == null) {
        paramType = computeParameterType();
    }
    this.parameterType = paramType;
    return paramType;
}

 

여기서 Class<?>를 반환하고 있는데 제네릭 와일드 카드도 한번 실험해볼까요?

@Test
void test() {
    // 예시 2: Class<?> 와일드카드
    System.out.println("\n=== Class<?> 와일드카드 ===");
    Class<?> wildcardType = String.class;
    System.out.println("값: " + wildcardType);
    // 출력: class java.lang.String
    System.out.println("타입 정보: " + wildcardType.getClass());
    // 출력: class java.lang.Class (정확한 파라미터 정보 없음)
}

 

음 역시 컴파일 시점에 String이라는 타입 정보가 사라집니다.

 

왜 이렇게 만든걸까요?

찾아보니 그 이유는

1. 자바 4 버전까지는 제네릭이 없었음

2. 하위 호환성을 위해 런타임 시점엔 제네릭 정보를 지움

3. 컴파일 할 때만 타입 체크 함

4. 런타임 시점에는 그냥 일반 클래스로 취급

한다고 합니다

 

그럼 아까 테스트 코드에서는

@DisplayName("@Login 어노테이션이 있고 타입이 AuthUser가 아니면 false를 반환한다")
@Test
void supportsParameterWithoutTypeAuthUser() {
    // given
    MethodParameter parameter = Mockito.mock(MethodParameter.class);

    given(parameter.hasParameterAnnotation(Login.class))
            .willReturn(true);
    given(parameter.getParameterType())
            .willReturn(Object.class);

    // when
    boolean result = authUserResolver.supportsParameter(parameter);

    // then
    assertThat(result).isFalse();
}

 

parameter.getParameterType()은 Class<?>를 리턴합니다.

근데 저는 Class<Object>를 리턴하려고 하죠?

 

여기서 문제가 발생합니다.

 

Class<?> 즉 아무거나 리턴할 수 있는데 그렇다고 아무거나 지정할 수 있는건 아닙니다. 만약에 내가 지정하려고 한 Object가 아닌 다른 클래스가 들어온다면?

 

즉 너무 엄격하게 타입 체킹을 하면서 이런 문제가 발생한 겁니다.

 

이런 이상한 이유로 오류가 나는게 사실 이해가 안되긴 하는데, 그니까 따져보면

아니 다 들어올 수 있는데 사실 니가 반환하려 하는게 아니면 어떻함? <- 이런 식으로 컴파일 시점에 오류가 나버리는 겁니다.

 

즉 앞에서 제네릭 실행 원리로 니주 다 깐거 사실 크게 관련 없다는 소립니다.

그냥 제네릭 관련해서 다시 공부해보면서 적은거에여.

?

 

그래서 이걸 어떻게 회피하냐!

 

일단 우린 이걸 컴파일 시점에만 피하면 된단 말입니다.

그래서

1. 결론 먼저 말해서 컴파일러 속이기

willReturn(Object.class)
                .given(parameter).getParameterType();

 

일단 willReturn을 앞에 박아서 타입 체크 무시하게 해버리고 given을 뒤에 붙혀서 본론을 나중에 말해버립니다.

 

진짜 얼탱이 없긴 한데 되긴 됩니다.

2. 명시적 타입 캐스팅으로 컴파일러 속이기

BDDMockito.<Class<?>>given(parameter.getParameterType())
                .willReturn(Object.class);

 

이렇게 명시적으로 얘는 Class<Object> 가 아닌 Class<?>를 반환해준다고 강제로 캐스팅 해주면 컴파일러를 속일 수 있습니다.

 

이런식으로 우리의 저능한 똑똑한 컴파일러를 속일 수 있으니 여러분들도 한번 이렇게 짜보세요.

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.