Spring MVC에서 컨트롤러 메서드를 작성할 때 @RequestBody의 유무가 클라이언트 요청 처리 방식에 영향을 미칩니다. 최근 한 프로젝트에서 @RequestBody를 생략했을 때 발생한 문제를 디버깅하고 해결한 과정을 정리해보았습니다.
아래와 같이 회원가입 요청을 처리하는 컨트롤러 메서드를 작성했습니다.
@PostMapping("/signup")
public ResponseEntity<?> signup(SignupRequestDTO requestDTO) {
userService.signup(requestDTO);
return ResponseEntity.ok(new ApiResponse("success", requestDTO));
}
클라이언트는 JSON 데이터를 POST 요청으로 전송했습니다.
POST /signup
Content-Type: application/json
{
"username": "user1",
"password": "password123"
}
하지만 서버 로그에 다음과 같은 에러가 발생했습니다:
2025-01-27T23:40:17.070+09:00 ERROR 23676 --- [BE] [nio-8082-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: rawPassword cannot be null] with root cause
java.lang.IllegalArgumentException: rawPassword cannot be null
@RequestBody의 역할
@RequestBody는 HTTP 요청의 바디에 포함된 JSON 데이터를 Java 객체로 변환합니다. 이를 위해 Spring의 HttpMessageConverter가 작동합니다.@RequestBody가 없으면 Spring은 요청 데이터를 기본적으로 쿼리 파라미터나 폼 데이터로 처리하려고 시도합니다. 하지만 클라이언트는 JSON 데이터를 요청 바디에 전송했기 때문에 변환이 이루어지지 않았습니다.DTO 필드 초기화 실패
@RequestBody가 없으므로 SignupRequestDTO 객체의 필드가 초기화되지 않고 모두 null로 설정되었습니다.passwordEncoder.encode(requestDTO.getPassword()))이 호출되면서, rawPassword가 null로 전달되어 IllegalArgumentException이 발생했습니다.클라이언트와 서버의 요청 처리 방식 불일치
우선 HTTP 요청 데이터가 제대로 전달되는지 확인하기 위해 요청의 Content-Type을 로깅했습니다.
@PostMapping("/signup")
public ResponseEntity<?> signup(HttpServletRequest request) {
System.out.println("Content-Type: " + request.getContentType());
return ResponseEntity.badRequest().build();
}
결과 로그:
Content-Type: application/json
요청은 JSON 형식으로 제대로 전달되고 있음을 확인했습니다.
@RequestBody 추가 후 동작 확인컨트롤러 메서드에 @RequestBody를 추가하여 요청 데이터를 매핑하도록 수정했습니다.
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody SignupRequestDTO requestDTO) {
userService.signup(requestDTO);
return ResponseEntity.ok(new ApiResponse("success", requestDTO));
}
클라이언트 요청:
POST /signup
Content-Type: application/json
{
"username": "user1",
"password": "password123"
}
수정 후 정상적으로 동작하며 요청 데이터가 DTO에 매핑되었습니다.
컨트롤러에서 @RequestBody를 추가하여 요청 데이터를 매핑하도록 설정했습니다.
@PostMapping("/signup")
@Operation(summary = "회원가입")
public ResponseEntity<?> signup(@RequestBody SignupRequestDTO requestDTO) {
userService.signup(requestDTO);
return ResponseEntity.ok(new ApiResponse("success", requestDTO));
}
클라이언트는 반드시 다음과 같은 JSON 데이터를 요청 바디에 포함해야 합니다.
{
"username": "user1",
"password": "password123"
}
헤더에는 Content-Type: application/json이 포함되어야 합니다.
SignupRequestDTO에 유효성 검사 어노테이션을 추가하여 입력 데이터를 검증합니다.
public class SignupRequestDTO {
@NotNull
private String username;
@NotNull
private String password;
// Getters and Setters
}
입력값이 잘못되었을 때 적절한 에러 메시지를 반환하기 위해 예외 처리를 추가합니다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return ResponseEntity.badRequest().body(errors);
}
만약 클라이언트가 JSON이 아닌 폼 데이터나 쿼리 파라미터를 사용해야 한다면 @RequestParam을 사용하여 요청 데이터를 처리할 수 있습니다.
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestParam String username, @RequestParam String password) {
SignupRequestDTO requestDTO = new SignupRequestDTO(username, password);
userService.signup(requestDTO);
return ResponseEntity.ok(new ApiResponse("success", requestDTO));
}
@RequestBody는 클라이언트 요청에서 JSON 데이터를 객체로 매핑할 때 반드시 필요합니다. 이 문제는 요청 데이터 형식과 컨트롤러 처리 방식의 불일치에서 발생한 것으로, @RequestBody를 추가하여 해결했습니다.
이번 디버깅 과정을 통해 요청 처리 방식과 데이터를 매핑하는 방법에 대한 이해를 깊게 할 수 있었습니다.