안녕하세요.
문제는 그렇게 시작됐습니다.
드디어 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<?>를 반환해준다고 강제로 캐스팅 해주면 컴파일러를 속일 수 있습니다.
이런식으로 우리의 저능한 똑똑한 컴파일러를 속일 수 있으니 여러분들도 한번 이렇게 짜보세요.
끝