spring 예외(Exception) 처리
사용자가 필수 파라미터를 빼고 요청을 보냄. 서버 내에서 데이터베이스 조회 오류. 그 외 예상치 못한 오류. 등등 예외가 발생하면 어떻게 처리해야 하는가? 기본적으로 스프링이 적절한 예외 페이지를 보여준다. 그러나 좀 더 자세하게 오류 정보를 전달하고시다면 따로 설정해주어야 한다.
예외가 발생하였을 때, 표현 방법에 따라 2가지로 나눌 수 있다. 웹에서 요청한 경우와 api를 요청한 경우이다. 각각 html, json으로 오류 정보를 응답한다.
1. html로 오류 표시 (웹 클라이언트 요청)
스프링에서는 resources/templates/error또는 resources/static/error디렉토리에 에러코드.html을 만들면 된다.
resources/templates/error/500.html resources/templates/error/5xx.html
resources/static/error/400.html resources/static/error/404.html
등등을 만들면 되는 것이다.
오류 페이지의 model에 timestamp, path, status, message, error, exception, errors, trace값들을 전달해주기 때문에 정보를 출력해줄 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>4xx 에러 </title>
</head>
<body>
<p th:text="|timestamp = ${timestamp}|"></p>
<p th:text="|path = ${path}|"></p>
<p th:text="|status = ${status}|"></p>
<p th:text="|message = ${message}|"></p>
<p th:text="|error = ${error}|"></p>
<p th:text="|exception = ${exception}|"></p>
<p th:text="|errors = ${errors}|"></p>
<p th:text="|trace = ${trace}|"></p>
</body>
</html>
resources/templates/error/4xx.html
위 그림과 같이 표시된다. 기본적으로 보안상 message, exception, errors, trace는 전달되지 않는다. 전달하고 싶다면 application.properties에서 설정해주면 된다. 그러나 보통 해커들에게 정보를 주는것을 막기 위해 정보들을 잘 주지 않는다.
서버 내에서 예외가 생겼을 경우, status 500으로 보인다. 그러나 해당 예외에 맞는 자세한 상태코드를 응답하고싶다면 @ExceptionHandler와 @ResponseStatus를 사용하면 된다.
@Controller
public class htmlController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "error/400";
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public String handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
return "error/missingRequestParameter";
}
@GetMapping("/aa")
public String aa() {
throw new IllegalArgumentException("잘못된 매개변수");
}
@GetMapping("/bb")
public String bb(@RequestParam String id) {
return "index";
}
}
/aa경로에서 어떤 오류가 생겨 IllegalArgumentException이 발생한다고 해보자. 그러면 원래는 사용자 화면에 status 500가 보이게 된다. 이 예외가 발생했을 때, status 400를 전달하고싶다면 @ExceptionHandler에 예외를 넣고 @ResponseStatus에 status를 등록하면 된다.
/bb경로는 id라는 파라미터를 받는데, 이 파라미터가 없다면 MissingServletRequestParameterException 예외가 발생하며 status 400을 응답한다. 그러나 파라미터가 없다는 정확한 정보전달을 위해 따로 페이지를 만들어 전달한다면 위와 같이 만들 수 있다.
2. json으로 오류 표시 (api 요청)
api요청은 보통 json으로 주고받는다. 예외가 발생했다면 오류 응답 스펙에 맞춰 클래스를 하나 생성해준다.
@AllArgsConstructor
@Getter @Setter
public class ErrorResult {
private String status;
private String message;
}
그리고 @ExceptionHandler와 @ResponseStatus를 사용하여 적절한 응답을 만들면 된다.
@RestController
@RequestMapping("/api")
public class ApiController {
@ResponseStatus(code= HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalArgumentException(IllegalArgumentException e) {
return new ErrorResult("bad", e.getMessage());
}
@GetMapping("/ex")
public String ex() {
throw new RuntimeException("ex");
}
@GetMapping("/bad")
public String bad() {
throw new IllegalArgumentException("잘못된 입력 값");
}
@GetMapping("/param")
public String param(@RequestParam String param) {
return "ok";
}
}
ApiController.class
{
"timestamp": "2025-04-17T09:01:48.929+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/ex"
}
/api/ex 응답
{
"status": "bad",
"message": "잘못된 입력 값"
}
/api/bad응답
이렇게 @ExceptionHandler를 사용하면 컨트롤러 단위로 예외를 처리할 수 있다.
검증 실패(@Valid/@Validated + @ModelAttribute/@RequestBody)시 @ExceptionHandler 사용법
원래 html을 반환하는 검증에서는 @Validated 매개변수 뒤에 BindingResult를 넣어야하지만, BindingResult를 생략한다면 MethodArgumentNotValidException이 발생한다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/member")
@Slf4j
public class MemberController {
private final MemberService memberService;
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult(); // 여기에 들어있음!
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return errors;
}
@PostMapping("/signup")
public String signup(@ModelAttribute @Validated MemberSignupForm memberSignupForm) {
return "회원가입 성공";
}
}
MethodArgumentNotValidException에 BindingResult가 들어있어 쉽게 에러 내용 전달이 가능하다.
3. 여러 컨트롤러 단위로 예외 처리
@ControllerAdvice 또는 @RestControllerAdvice를 사용해 여러 컨트롤러 단위로 예외를 처리할 수 있다.
html에 @ControllerAdvice를, api에 @RestControllerAdvice를 지정하면 된다.
@RestControllerAdvice(annotations = RestController.class)
public class ExRestControllerAdvice {
@ResponseStatus(code= HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalArgumentException(IllegalArgumentException e) {
return new ErrorResult("bad", e.getMessage());
}
}
api의 예외처리부분을 별도의 클래스로 뺀 것이다. RestController 어노테이션이 붙은 클래스에 적용된다.
@ControllerAdvice(annotations = Controller.class)
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return "error/400";
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public String handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
return "error/missingRequestParameter";
}
}
html 예외처리부분을 별도의 클래스로 뺀 것이다. Controller 어노테이션이 붙은 클래스에 적용된다.
@ControllerAdvice 와 @RestControllerAdvice는 범위를 지정할 수 있는다. 어노테이션, 패키지, 클래스 를 지정할 수 있다.
@ControllerAdvice(annotations = Controller.class) => 어노테이션 지정
@ControllerAdvice("org.example.controller") => 패키지 지정
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) => 클래스 지정