MVC 활용(28) : 예외 처리 핸들러 - @ExceptionHandler

28. 예외 처리 핸들러 - @ExceptionHandler

특정 예외가 발생한 요청을 처리하는 핸들러 정의

  • 지원하는 메소드 아규먼트 (해당 예외 객체, 핸들러 객체, ...)
  • 지원하는 리턴 값
  • REST API의 경우 응답 본문에 에러에 대한 정보를 담아주고, 상태 코드를 설정하려면 ResponseEntity를 주로 사용한다.

커스텀한 에러 만들기

public class EventException extends RuntimeException{
}

커스텀한 에러가 발생했을 때 특정한 메시지와 함께 뷰를 보여주고 싶다.

@Controller
@SessionAttributes("event")
public class EventController {

    @ExceptionHandler
    public String eventErrorHandler(EventException exception, Model model){
        model.addAttribute("message", "event error");
        return "error";
    }

    @InitBinder
    public void initEventBinder(WebDataBinder webDataBinder){
        webDataBinder.setDisallowedFields("id");
        webDataBinder.addValidators(new EventValidator());
    }

    @ModelAttribute
    public void categories(Model model){
        model.addAttribute("categories", List.of("study", "seminar", "hobby", "social"));
    }

     @GetMapping("/events/form/name")
    public String eventsFormName(Model model) {
        throw new EventException();
//        model.addAttribute("event", new Event());
//        자동으로 세션에 넣어줌
//        return "events/form-name";
    }

    @PostMapping("/events/form/name")
    public String eventsFormNameSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "/events/form-name";
        }
        return "redirect:/events/form/limit";
    }
    
    @GetMapping("/events/form/limit")
    public String eventsFormLimit(@ModelAttribute @Valid Event event, Model model) {
        model.addAttribute("event", event);
        // 자동으로 세션에 넣어줌
        return "events/form-limit";
    }
    
    @PostMapping("/events/form/limit")
    public String eventsFormLimitSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult, SessionStatus sessionStatus, RedirectAttributes attributes){
        if(bindingResult.hasErrors()){
            return "/events/form-limit";
        }
        sessionStatus.setComplete();
        // 데이터베이스에서 save 했다고 가정
        attributes.addFlashAttribute("newEvent", event);
        return "redirect:/events/list";
    }
    
    @GetMapping("/events/list")
    public String getEvents(Model model, 
                            @SessionAttriubte LocalDateTime visitTime){
        System.out.println(visitTime);
        
        Event spring = new Event();
        spring.setName("spring");
        spring.setLimit(10);
        
        Event newEvent = (Event) model.asMap().get("newEvent");
        
        // 데이터베이스에서 읽어왔다고 가정한다.
        List<Event> eventList = new ArrayList<>();
        eventList.add(spring);
        eventList.add(newEvent);
        model.addAttribute("eventList", eventList);
        return "/events/list";
    }
}

에러 페이지

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Error Page</title>
</head>
<body>

<div th:if="${message}">
    <h2 th:text="${message}"/>
</div>
</body>
</html>

또한 가장 구체적인 ExceptionHandler가 매핑된다.

@Controller
@SessionAttributes("event")
public class EventController {

    @ExceptionHandler
    public String eventErrorHandler(EventException exception, Model model){
        model.addAttribute("message", "event error");
        return "error";
    }
    
    @ExceptionHandler
    public String runtimeExceptionHandler(RuntimeException exception, Model model){
        model.addAttribute("message", "runtime error");
        return "error";
    }

    @InitBinder
    public void initEventBinder(WebDataBinder webDataBinder){
        webDataBinder.setDisallowedFields("id");
        webDataBinder.addValidators(new EventValidator());
    }

    @ModelAttribute
    public void categories(Model model){
        model.addAttribute("categories", List.of("study", "seminar", "hobby", "social"));
    }

     @GetMapping("/events/form/name")
    public String eventsFormName(Model model) {
        throw new EventException();
//        model.addAttribute("event", new Event());
//        자동으로 세션에 넣어줌
//        return "events/form-name";
    }

    @PostMapping("/events/form/name")
    public String eventsFormNameSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "/events/form-name";
        }
        return "redirect:/events/form/limit";
    }
    
    @GetMapping("/events/form/limit")
    public String eventsFormLimit(@ModelAttribute @Valid Event event, Model model) {
        model.addAttribute("event", event);
        // 자동으로 세션에 넣어줌
        return "events/form-limit";
    }
    
    @PostMapping("/events/form/limit")
    public String eventsFormLimitSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult, SessionStatus sessionStatus, RedirectAttributes attributes){
        if(bindingResult.hasErrors()){
            return "/events/form-limit";
        }
        sessionStatus.setComplete();
        // 데이터베이스에서 save 했다고 가정
        attributes.addFlashAttribute("newEvent", event);
        return "redirect:/events/list";
    }
    
    @GetMapping("/events/list")
    public String getEvents(Model model, 
                            @SessionAttriubte LocalDateTime visitTime){
        System.out.println(visitTime);
        
        Event spring = new Event();
        spring.setName("spring");
        spring.setLimit(10);
        
        Event newEvent = (Event) model.asMap().get("newEvent");
        
        // 데이터베이스에서 읽어왔다고 가정한다.
        List<Event> eventList = new ArrayList<>();
        eventList.add(spring);
        eventList.add(newEvent);
        model.addAttribute("eventList", eventList);
        return "/events/list";
    }
}

여러 에러를 같이 처리하고 싶은 경우 : 둘다 받을 수 있는 상위 타입으로 정의

@Controller
@SessionAttributes("event")
public class EventController {

    @ExceptionHandler({EventException.class, RuntimeException.class})
    public String eventErrorHandler(RuntimeException exception, Model model){
        model.addAttribute("message", "runtime error");
        return "error";
    }

    @InitBinder
    public void initEventBinder(WebDataBinder webDataBinder){
        webDataBinder.setDisallowedFields("id");
        webDataBinder.addValidators(new EventValidator());
    }

    @ModelAttribute
    public void categories(Model model){
        model.addAttribute("categories", List.of("study", "seminar", "hobby", "social"));
    }

     @GetMapping("/events/form/name")
    public String eventsFormName(Model model) {
        throw new EventException();
//        model.addAttribute("event", new Event());
//        자동으로 세션에 넣어줌
//        return "events/form-name";
    }

    @PostMapping("/events/form/name")
    public String eventsFormNameSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            return "/events/form-name";
        }
        return "redirect:/events/form/limit";
    }
    
    @GetMapping("/events/form/limit")
    public String eventsFormLimit(@ModelAttribute @Valid Event event, Model model) {
        model.addAttribute("event", event);
        // 자동으로 세션에 넣어줌
        return "events/form-limit";
    }
    
    @PostMapping("/events/form/limit")
    public String eventsFormLimitSubmit(@ModelAttribute @Valid Event event, BindingResult bindingResult, SessionStatus sessionStatus, RedirectAttributes attributes){
        if(bindingResult.hasErrors()){
            return "/events/form-limit";
        }
        sessionStatus.setComplete();
        // 데이터베이스에서 save 했다고 가정
        attributes.addFlashAttribute("newEvent", event);
        return "redirect:/events/list";
    }
    
    @GetMapping("/events/list")
    public String getEvents(Model model, 
                            @SessionAttriubte LocalDateTime visitTime){
        System.out.println(visitTime);
        
        Event spring = new Event();
        spring.setName("spring");
        spring.setLimit(10);
        
        Event newEvent = (Event) model.asMap().get("newEvent");
        
        // 데이터베이스에서 읽어왔다고 가정한다.
        List<Event> eventList = new ArrayList<>();
        eventList.add(spring);
        eventList.add(newEvent);
        model.addAttribute("eventList", eventList);
        return "/events/list";
    }
}

Rest API의 경우 상태코드와 에러 메시지를 주기 위해 ResponseEntity를 주로 사용한다.

@RestController     
@RequestMapping("/api/events")
public class EventApi {

    @ExceptionHandler
    public ResponseEntity errorHandler(){
        return ResponseEntity.badRequest().body("cant't create event as ...");
    }

     @PostMapping
    public ResponseEntity<Event> createEvent(@RequestBody @Valid Event event, BindingResult bindingResult){
        // save event
        if(bindingResult.hasErrors()){
            bindingResult.getAllErrors().forEach(error -> {
                System.out.println(error);
           });
            return ResponseEntity.badRequest().build();
        }
        return ResponseEntity.ok(event);
        //return new ResponseEntity<Event>(event, HttpStatus.CREATED); 200이 아닌 201 Created 응답
        return ResponseEntity.ok(event);
    }
}

참고

좋은 웹페이지 즐겨찾기