[SpringBoot] 스프링부트3 Name for argument of type not specified 에러(Parameter Name Retention)
1. 스프링부트3 Name for argument of type not specified 에러
(Parameter Name Retention)
[ SpringBoot3 Parameter Name Retention ]
스프링부트3로 버전이 올라가면서 정말 많은 변경사항이 생겼는데, 그 중 하나가 Parameter Name Retention 관련된 부분이다. 해당 내용은 LocalVariableTableParameterNameDiscoverer 클래스가 스프링 프레임워크 6.0에서 deprecated되고, 스프링 프레임워크 6.1에서 삭제되었다는 것이다. 해당 클래스는 어떠한 역할을 하고 있으며, 어떠한 영향을 미치는지 살펴보도록 하자.
LocalVariableTableParameterNameDiscoverer 클래스에 대하여
스프링에서 파라미터의 이름을 가져오는 방법이 여러 가지 있다. 해당 역할을 추상화한 인터페이스가 ParameterNameDiscoverer이며, 구현체로 LocalVariableTableParameterNameDiscoverer과 StandardReflectionParameterNameDiscoverer이 존재한다.
LocalVariableTableParameterNameDiscoverer는 LocalVariableTable이라는, 자바 바이트코드가 메서드의 로컬 변수 정보를 저장하는 테이블을 통해 값을 반환한다.
예를 들어 다음과 같은 클래스가 존재한다고 하자.
public class Example {
public void exampleMethod(int param1, String param2) {
int localVar = param1 + 1;
System.out.println(param2);
}
}
이 Java 코드를 컴파일하면 다음과 같은 바이트코드가 생성되며, LocalVariableTable은 다음과 같이 포함된다. 따라서 LocalVariableTableParameterNameDiscoverer는 바이트코드 파싱 결과로부터 파라미터 이름을 얻어온다고 볼 수 있다.
public void exampleMethod(int, java.lang.String);
descriptor: (ILjava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1
1: iconst_1
2: iadd
3: istore_3
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_2
8: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this LExample;
0 12 1 param1 I
0 12 2 param2 Ljava/lang/String;
4 8 3 localVar I
문제는 해당 기능이 Deprecated 되었다는 점인데, 관련 히스토리를 찾아보면 Native Image(네이티브 이미지)를 지원하기 위한 것으로 보인다. 따라서 이에 대한 대안으로 StandardReflectionParameterNameDiscoverer이 사용되기 시작했다.
StandardReflectionParameterNameDiscoverer에 대하여
StandardReflectionParameterNameDiscoverer는 리플렉션을 통해 파라미터 이름을 가져오는 기술이다. 문제는 리플렉션으로부터 해당 값을 가져오려면 컴파일 시에 -parameters 옵션을 반드시 추가해주어야 한다는 점이다.
예를 들어, 다음과 같은 코드는 이제 -parameters 옵션 없이 스프링에서 동작하지 않는다.
@RestController
class HelloController {
@GetMapping("/hello")
fun hello(@RequestParam name: String): String {
return "Hello $name"
}
}
만약 Gradle 기반 프로젝트에서 해당 내용을 재현하려면 Gradle이 아닌 방식으로 애플리케이션을 실행시켜야 한다. 해당 설정은 다음과 같이 실행이 IDE 기반으로 되도록 설정 해주면 된다.
위의 API를 호출하면 다음과 같은 에러가 발생하게 된다.
2024-06-20T23:25:12.862+09:00 ERROR 33739 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.] with root cause
java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.updateNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:187) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.getNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:162) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:108) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:224) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:178) ~[spring-web-6.1.8.jar:6.1.8]
위의 문제를 해결하려면 다음과 같이 파라미터의 이름을 명시해주어야 한다.
@RestController
class HelloController {
@GetMapping("/hello")
fun hello(@RequestParam("name") name: String): String {
return "Hello $name"
}
}
또는 자바 컴파일 시에 -parameters 옵션을 추가하여, JDK8 이상에서 컴파일 시에 Reflection API로 파라미터 정보를 가지고 올 수 있도록 컴파일된 클래스에 정보를 추가해줄 수 있다.
만약 Gradle 기반으로 빌드 및 배포를 한다면 -parameters 옵션을 추가하지 않아도 정상적으로 처리되는데, 그 이유는 Gradle 플러그인과 SpringBoot의 추가적인 설정 덕분이다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.6'
id 'io.spring.dependency-management' version '1.1.3'
}
parameters 옵션은 SpringBoot가 제공하는 Gradle의 java 플러그인 때문이 처리해준다. 해당 플러그인을 사용하면 Java 컴파일의 -parameters 옵션이 자동 추가된다. 관련 코드는 여기서 확인할 수 있다.
[ SpringBoot3 Parameter Name Retention ]
LocalVariableTableParameterNameDiscoverer 클래스는 스프링 6.0에서 deprecated 되었고, 6.1에서 최종 삭제되었다. 이를 스프링부트 기준으로 표현하면 다음과 같다.
- 스프링 부트 3.0(스프링 6.0에서 deprecated)
- 스프링 부트 3.1(스프링 6.0에서 deprecated)
- 스프링 부트 3.2(스프링 6.1에서 removed)
따라서 스프링 부트 3.2부터는 해당 클래스가 완전히 삭제되어 관련 문제가 발생할 수 있으므로 주의해주도록 하자. 개인적으로는 일관된 빌드 및 실행을 위해 항상 Gradle을 통해 실행하는 것이 바람직하다고 생각한다.