Spring Boot와 Exception 처리
Exception 처리란?
Spring Boot에서 에러가 발생할 경우 보통은 White Label 페이지를 보여준다.
추가적으로 400, 404와 같은 에러코드를 보여주는데, 이것만으로는 어디서 어떤 문제가 발생했는지 파악하는데 충분하지 않을 수 있다.
때문에 Web Application 입장에서는 이러한 에러를 한 곳에서 + 어디서 무슨 문제가 발생했는지 알려주면 보다 쉽게 에러를 처리할 수 있다.
대표적으로 Spring Boot에서 제공하는 Exception 처리 Annotation은 아래와 같다.
@ControllerAdvice (@RestControllerAdvice) |
Global 예외 처리 및 특정 Package / Controller 예외 처리 * REST API를 사용한다면 RestControllerAdvice 사용을 권장 |
@ExceptionHandler | 특정 Controller의 예외처리 |
코드 설명
[ User.java ]
package com.example.exception.dto;
// import 생략
public class User {
@NotEmpty
@Size(min = 1, max = 10)
private String name;
@Min(1)
@NotNull
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
보다시피 name과 age는 not null로 annotation을 걸어뒀다.
age가 Integer로 선언됐다는것에 유의해야하는데, Ineger는 null값으로 입력이 가능하다.
[ ApiController.java ]
package com.example.exception.controller;
// import 생략
@RestController
@RequestMapping("/api/user")
public class ApiController {
@GetMapping("")
public User get(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age){
User user = new User();
user.setName(name);
user.setAge(age);
int a = 10+age;
return user;
}
@PostMapping("")
public User post(@Valid @RequestBody User user){
System.out.println(user);
return user;
}
}
User의 get 메소드에 name과 age는 필수값으로 지정하지 않았다.
이 상태로 Get을 요청하되, name과 age 파라미터를 빈값으로 보내면 다음과 같이 NullPointerException이 터진다.
null언급이 되지만 정확히 어디서 무슨 값이 null이 된건지 확인하는데는 불편함이 있다.
추가로 Post 메소드와 관련하여 테스트를 해보자.
위와같이 name에 null값을 넣어 Post를 요청하면 400 에러를 출력하고, console에서는 다음과 같은 log를 출력해준다.
2021-12-07 11:33:48.549 WARN 1448 --- [nio-9090-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.exception.dto.User com.example.exception.controller.ApiController.post(com.example.exception.dto.User) with 3 errors: [Field error in object 'user' on field 'name': rejected value []; codes [NotEmpty.user.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [비어 있을 수 없습니다]] [Field error in object 'user' on field 'age': rejected value [0]; codes [Min.user.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],1]; default message [1 이상이어야 합니다]] [Field error in object 'user' on field 'name': rejected value []; codes [Size.user.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name],10,1]; default message [크기가 1에서 10 사이여야 합니다]] ]
서버에서는 위와같이 에러를 파악할 수 있지만, 클라이언트 쪽에서는 "400"에러만 확인할 수 있어서 다소 불편함을 느낄 수 있다.
[ GlobalControllerAdvice.java ]
package com.example.exception.advice;
// import 생략
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 해당 항목의 하위에 있는 모든 에러를 잡을거다.
public class GlobalControllerAdvice {
@ExceptionHandler(value = Exception.class)
public ResponseEntity exception(Exception e){
System.out.println(e.getClass().getName());
System.out.println("------------------------");
System.out.println(e.getLocalizedMessage());
System.out.println("------------------------");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
}
}
이제 Exception을 한 곳에서 처리하고자 GlobalControllerAdvice를 만들었다.
위와 같이 작성하고 Post 요청을 하면 다음과 같이 콘솔에 출력된다.
org.springframework.web.bind.MethodArgumentNotValidException
------------------------
Validation failed for argument [0] in public com.example.exception.dto.User com.example.exception.controller.ApiController.post(com.example.exception.dto.User) with 3 errors: [Field error in object 'user' on field 'age': rejected value [0]; codes [Min.user.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],1]; default message [1 이상이어야 합니다]] [Field error in object 'user' on field 'name': rejected value []; codes [NotEmpty.user.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [비어 있을 수 없습니다]] [Field error in object 'user' on field 'name': rejected value []; codes [Size.user.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name],10,1]; default message [크기가 1에서 10 사이여야 합니다]]
------------------------
보다시피 해당 클래스가 어디에서 잘못됐는지 알려주고 있다. (MethodArgumentNotValidException)
여기서 추가로 방금 잡아낸 MethodArgumentNotValidException에 대한 예외를 처리해보자.
package com.example.exception.advice;
// import 생략
@RestControllerAdvice(basePackages = "com.example.exception.controller") // 해당 항목의 하위에 있는 모든 에러를 잡을거다.
public class GlobalControllerAdvice {
@ExceptionHandler(value = Exception.class)
public ResponseEntity exception(Exception e){
System.out.println(e.getClass().getName());
System.out.println("------------------------");
System.out.println(e.getLocalizedMessage());
System.out.println("------------------------");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
}
// 특정메소드 "MethodArgumentNotValidException"의 예외를 잡고싶다
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidation(MethodArgumentNotValidException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
MethodArgumentNotValidException에 대한 원인을 Body에서 출력해주고 있다.
에러 로그가 예쁘게 출력되지는 않지만, 얼핏보면 어디에서 무슨 문제가 발생했는지는 파악할 수 있다.
ControllerAdvice(RestContrllerAdvice) vs ExceptionHandler
만약 양쪽 모두에 동일한 에러 핸들링을 저장할 경우, 결론적으로 ExceptionHandler에서 실행이 된다.
둘중 우선순위는 ExceptionHandler가 더 높으며, ExceptionHandler에서 에러 핸들링이 동작할 경우 RestControllerAdvice에서 입력된 동일한 에러핸들링은 작동되지 않는다.