본문 바로가기
빌드노트 검색

빌드노트

@RequestParam 이름을 생략했더니 빌드 후 요청이 실패한다

Spring MVC에서 이름을 생략한 @RequestParam이 빌드 후 실패할 때, 요청 값보다 MethodParameters를 먼저 확인한다. 같은 컨트롤러를 -parameters 유무로 컴파일해 reflection·MockMvc 결과를 비교하고, 명시적 요청 키와 컴파일 설정 중 선택할 기준을 설명한다.

Spring MVC의 @RequestParam String q가 로컬 실행에서는 동작하다가 다른 환경에서 빌드한 뒤 이름을 찾지 못한다면, 요청의 q보다 실패한 클래스에 Java 매개변수 이름이 남아 있는지 먼저 확인한다. 애너테이션에 요청 키를 생략하면 Spring은 메서드 매개변수 이름을 알아내야 한다. 소스에 q가 보인다는 사실만으로 실행 중인 클래스에서도 그 이름을 읽을 수 있는 것은 아니다.

별도 예제에서 q=spring을 똑같이 전달하고 컴파일 옵션만 바꾸자 이 차이가 나타났다. -parameters가 없는 클래스는 이름 해석 예외를 던졌고, 이 옵션을 넣거나 @RequestParam("q")로 요청 키를 명시한 경우에는 정상 응답했다.

소스 대신 실패한 class의 이름 정보를 확인한다

예외에 Name for argument of type [java.lang.String] not specified가 보이고 컴파일러의 -parameters 옵션을 확인하라고 나온다면, 먼저 그 컨트롤러의 .class를 조사한다. JDK가 설치된 PowerShell에서 조사할 SearchController.class가 있는 디렉터리로 이동한 뒤 다음 명령을 실행한다. 파일명은 자신의 컨트롤러에 맞춘다. 패키징된 파일을 조사한다면 해당 빌드 산출물에서 클래스를 꺼내 준비한다. IDE가 따로 컴파일한 클래스와 실패한 배포본을 혼동하지 않아야 한다.

javap -v -p .\SearchController.class

출력에서 문제가 난 메서드의 선언을 찾고, 그 메서드에 속한 MethodParameters를 확인한다. 아래는 -parameters를 넣어 컴파일한 예제의 search(String)에서 해당 부분만 발췌한 것이다.

    MethodParameters:
      Name                           Flags
      q

-parameters를 빼면 이 메서드의 MethodParameters 자체가 없었다. LocalVariableTable에 q가 보이는 것과는 구별해야 한다. 이 디버그 정보만으로 충분한지는 Spring 버전에 따라 달라진다. javap 공식 문서 (새 탭에서 열림)는 클래스 파일을 직접 지정하는 방법과 상세 출력 옵션을 설명한다.

클래스를 로드할 수 있는 기존 테스트에서도 이름의 존재를 확인할 수 있다. 아래 두 줄은 예외를 던질 수 있는 Java 테스트 메서드 안에서 실행한다. SearchController, search, String.class를 실제 컨트롤러·메서드명·인자 타입에 맞춘다. 인자가 여러 개라면 타입 목록과 조사할 인덱스도 맞춰야 한다.

var p = SearchController.class.getDeclaredMethod("search", String.class).getParameters()[0];
System.out.println(p.getName() + " / " + p.isNamePresent());

예제의 출력은 옵션이 없을 때 arg0 / false, 있을 때 q / true였다. Parameter API (새 탭에서 열림)에 따르면 isNamePresent()는 MethodParameters에 이름이 있는지를 나타낸다. 이름 정보가 없을 때 getName()이 만든 arg0는 원래 소스 이름이 아니다. 실제 비교에서도 요청 키를 arg0로 바꾸는 것으로는 이름 해석 예외가 사라지지 않았다.

같은 요청을 보내고 컴파일 옵션만 바꿨다

Windows의 OpenJDK 17.0.2, Spring Framework 6.2.11, Servlet API 6.0.0에서 작은 SearchController를 따로 컴파일했다. 다음은 비교에 쓴 바인딩 선언이다. 두 메서드는 @RestController 클래스에 들어가며, MVC 애너테이션은 org.springframework.web.bind.annotation 패키지의 것을 사용한다.

@GetMapping("/search")
public String search(@RequestParam String q) {
    return q;
}

@GetMapping("/search-explicit")
public String searchExplicit(@RequestParam("q") String query) {
    return query;
}

각 결과물을 별도로 로드한 standalone MockMvc에서 /search에 .param("q", "spring")을 전달했다. 실제 서버에 접속한 결과는 아니다. 아래에서 이름과 보존 여부는 Java reflection의 출력이며, 응답 본문은 spring이었다.

컴파일 옵션              이름 / 보존 여부    /search 결과
-g                      arg0 / false        이름 해석 예외
-g -parameters          q / true            200, spring
-g:none                 arg0 / false        이름 해석 예외
-g:none -parameters     q / true            200, spring

-g만 준 클래스에는 LocalVariableTable의 q가 있었다. 그런데도 Spring의 기본 이름 발견기는 이름을 찾지 못했다. 반대로 디버그 정보를 없앤 -g:none -parameters는 정상 처리됐다. javac 공식 문서 (새 탭에서 열림)는 -g를 디버그 정보 생성 옵션으로, -parameters를 reflection으로 읽을 매개변수 이름을 저장하는 옵션으로 구분한다.

명시적으로 이름을 적은 /search-explicit에는 같은 q=spring을 보내자 네 조건 모두 200과 spring이 나왔다. Java 인자 이름은 query지만 요청 키는 애너테이션의 q였다. -parameters가 없어 reflection에 arg0만 보이는 클래스에서도 이 바인딩은 동작했다. Spring MVC의 RequestParam 문서 (새 탭에서 열림) 역시 요청 키를 애너테이션에 적는 형태를 제시한다.

요청 키가 정말 빠진 경우도 비교했다. 이름 정보가 있는 /search와 모든 /search-explicit은 q를 보내지 않으면 400과 MissingServletRequestParameterException을 남겼다. /search-explicit에 query=spring만 보낸 경우도 같았다. 반면 이름 정보가 없는 /search는 q의 유무와 관계없이 IllegalArgumentException이 원인인 ServletException을 던졌다. 이 경우 MockMvc 호출이 예외로 끝났으므로 응답 상태를 500으로 측정했다고 표현할 수는 없다.

이는 요청 값의 누락이나 숫자 변환 실패보다 앞선 문제다. 6.2.11의 인자 해석 코드 (새 탭에서 열림)는 먼저 애너테이션 또는 메서드에서 이름을 결정하고, 그다음 요청 값을 찾는다. 이름 결정 단계가 실패했다면 required=false나 defaultValue로 값의 누락 정책을 바꾸는 것으로는 해결되지 않는다. 이 두 옵션을 바꾼 요청은 별도 실행하지 않았으며, 처리 순서에 따른 설명이다.

예전에는 디버그 이름으로도 되던 이유

이름을 생략한 코드가 이전 Spring에서 동작했다는 설명에는 버전 조건이 필요하다. 6.0.13의 기본 이름 발견기 구현 (새 탭에서 열림)에는 일반 JVM에서 reflection으로 이름을 못 찾으면 LocalVariableTableParameterNameDiscoverer를 사용하는 경로가 있다. 6.1 릴리스 노트 (새 탭에서 열림)는 이 클래스가 6.1에서 제거됐으며 Java 코드를 -parameters로 컴파일하라고 안내한다.

따라서 “디버그 정보를 켰으니 된다”는 예전 조건을 6.1 이후에 그대로 적용할 수 없다. 이번 실행은 6.2.11에서 이 차이를 확인한 것이며, 6.0 런타임과의 비교는 하지 않았다. 로컬과 다른 환경의 차이를 조사할 때는 컴파일 옵션뿐 아니라 실제 실행 중인 Spring 버전도 함께 맞춰 봐야 한다.

요청 키를 명시하거나 컴파일 경로를 맞춘다

요청 키가 q로 정해져 있다면 위의 @RequestParam("q")처럼 적을 수 있다. 이 인자 바인딩은 Java 매개변수 이름 보존 여부에 의존하지 않으며, 인자 이름을 query로 바꾸어도 요청 키는 q로 유지된다.

이름을 생략하는 규칙을 유지하려면 컨트롤러 소스를 컴파일하는 모듈에 -parameters가 적용돼야 한다. Maven이라면 그 모듈의 기존 maven-compiler-plugin 설정에 아래 옵션을 반영하는 방식이 6.1 릴리스 노트에 나와 있다. 이미 플러그인 설정이 있으면 같은 항목에 병합한다. 이 parameters 옵션은 Maven Compiler Plugin 문서 (새 탭에서 열림)에 따르면 3.6.2부터 지원한다.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <parameters>true</parameters>
    </configuration>
</plugin>

Gradle의 Java 컴파일도 JavaCompile 작업에 -parameters를 전달하도록 설정한다. 구체적인 DSL 예시는 같은 릴리스 노트에서 확인할 수 있다. IDE와 CI가 서로 다른 컴파일 경로를 쓴다면 양쪽 설정을 확인하고, 변경 후 만들어진 최종 클래스에서 다시 MethodParameters를 읽는다. 설정 파일에 옵션을 적은 사실과 실패한 프로세스가 그 결과물을 로드한 사실은 따로 확인해야 한다.

이번 비교는 javac를 직접 실행했으므로 Maven·Gradle 설정의 실제 적용은 검증하지 않았다. MockMvc는 실행 중인 서버 없이 MVC 처리를 검사하는 도구 (새 탭에서 열림)다. 실제 배포 서버의 공통 예외 응답, Kotlin, 바이트코드를 바꾸는 Java agent는 확인하지 않았다. 대상 메서드에 이미 올바른 이름 정보가 있다면, 이번 누락 사례로 단정하지 말고 예외가 가리키는 인자와 실제 로드한 클래스를 다시 대조한다.

← 전체 글 보기