@ControllerAdvice 클래스에 @ResponseBody가 붙어있는지 확인해보세요!
간단하게 유저를 하나 생성하는 api를 구현했고 이를 테스트해 보았다.
@RestController
@RequestMapping("api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("register")
public void register(@RequestBody @Valid RegisterRequest request) {
userService.register(request);
}
}
컨트롤러 코드는 단순하게 서비스 레이어에 요청을 위임한다.
그리고 RegisterRequest dto 클래스에는 여러 검증 어노테이션을 사용했다.
@NoArgsConstructor
@Getter
public class RegisterRequest {
@NotBlank(message = "이메일은 필수 값입니다.")
@Size(max = 255, message = "이메일의 최대 길이는 255자입니다.")
private String email;
@NotBlank(message = "비밀번호는 필수 값입니다.")
@Size(max = 255, message = "비밀번호의 최대 길이는 255자입니다.")
private String password;
}
여기서 예외가 발생하면 ExceptionController가 예외를 잡아서 처리하도록 했다.
@ControllerAdvice
@Slf4j
public class ExceptionController {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ExceptionResponse handleException(Exception e) {
log.error(e.getMessage());
return new ExceptionResponse(e.getMessage());
}
그리고 예외를 올바르게 던지는지 확인하기 위해 다음처럼 테스트해 보았다.
POST http://localhost:8080/api/v1/users/register
Content-Type: application/json
{
"email" : "",
"password" : "sdf"
}
email값은 공백이나 빈 값이 될수 없기 때문에 예외가 발생하고 이 예외를 ExceptionController에서 잡아서 적절한 응답을 만들어서 보내주는것이다.
근데 400(BAD_REQUEST) 예외가 아닌 500(SERVER_ERROR) 가 응답되었고, view를 찾을 수 없다는 로그가 찍혔다.
{
"timestamp": "2022-12-12T06:18:05.660+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/v1/users/register"
}
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [api/v1/users/register], template might not exist or might not be accessible by any of the configured Template Resolvers
우선 응답 데이터를 변환하고 처리하는 역할을 하는 ReturnValueHandler
부터 디버깅해보았다.
dispatcherServlet에서 ModelAndView 객체의 view가 null이기 때문에 요청 url을 기본 뷰 이름으로 사용한다.
그리고 render를 호출한다.
view 렌더링을 담당하는 view 객체의 render가 호출되고 현재 프로젝트에서는 thymeleaf를 사용하고 있어서 ThymeleafView.renderFragment()가 호출되었다.
하지만 해당 이름을 가진 template이 존재하지 않기 때문에 예외가 발생하게 된다.
@RestController 어노테이션 내부에 @ResponseBody가 있고 그래서 body에 직접 데이터를 넣어서 보낸다고 명시한 것이나 똑같은데 왜 실패 응답을 보낼 때는 view를 찾고 성공 응답일 땐 제대로 응답이 가는 것인지 의아했다.
그래서 성공 응답을 디버깅해보니
성공 응답시에는 RequestResponseBodyMethodProcessor
가 사용됐다.
실패 응답시에는ServletModelAttributeMethodProcessor
가 사용됐다.
실패 응답 시에 RequestResponseBodyMethodProcessor를 확인도 해보기 전에 ServletModelAttributeMethodProcessor에서 if 문이 true가 되고 그대로 반환하게 되는 것이다.
왜 이런 차이가 생기는 걸까?
성공 응답일 경우를 다시 디버깅해보았다.
RequestResponseBodyMethodProcessor에서
@ResponseBody 어노테이션이 붙어있는지 확인한다.
여기서 문제가 발생했던 것이다.
예외를 잡아서 응답을 만들어 반환하는 ExceptionController에는 @ResponseBody 어노테이션이 없다.(@ResponseBody 어노테이션이 붙은 곳은 UserController이다.)
그래서 ErrorController에 @ResponseBody를 붙이고 테스트해 보았다.
기대했던 것처럼 이번엔 응답이 성공적으로 나온 것을 확인할 수 있다.
HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 12 Dec 2022 07:17:58 GMT
Connection: close
{
"message": "Validation failed for argument [0] in public void com.example.smilegateauthserver.user.controller.UserController.register(com.example.smilegateauthserver.user.dto.RegisterRequest): [Field error in object 'registerRequest' on field 'email': rejected value []; codes [NotBlank.registerRequest.email,NotBlank.email,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [registerRequest.email,email]; arguments []; default message [email]]; default message [이메일은 필수 값입니다.]] "
}