spring, ControllerAdvice와 RestControllerAdivce의 동시 적용의 문제와 해소

rest api와 view 렌더링 컨트롤러가 동시에 있을 때, 각각의 예외 어드바이스 적용이 안된다.

  • 한 프로젝트에 rest api와 view 렌더링 컨트롤러 두 개가 있다. 가능하면 분리하면 좋을 테지만 일단은 한 프로젝트와 한 어플리케이션에 동작한다.
  • 컨트롤러에 대한 ExceptionHandler를 @ControllerAdvice와 @RestControllerAdvice로 구현하였다.
  • 그런데 문제가 발생했다. 모든 예외가 @RestControllerAdvice를 통해 동작하였다.

  • rest와 view를 위한 각각의 어드바이스
@Slf4j
@ControllerAdvice
public class ViewExceptionHandler {
    @ExceptionHandler
    public void exException(Exception e, HttpServletResponse response) {
        log.info("ViewExceptionHandler working! exception message : {}", e.getMessage());
        response.setStatus(444);
        HtmlRenderingUtil.staticViewWriter(response, "static/view/myerror.html"); // response.getWriter() html을 렌더링하는 로직.
    }
}

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public Map<String, Object> exException(Exception e) {
        log.info("RestExceptionHandlerForTest working! exception message : {}", e.getMessage());
        return Map.of("message", "api error 발생! for test!~!");
    }
}
  • RestController와 Controller

@RestController
@RequestMapping("/rest")
public class RestsController {
    @GetMapping("/api")
    public ResponseEntity<Map<String, String>> greeting(){
        throw new RuntimeException("rest api 요청!");
    }
}

@Slf4j
@Controller
@RequestMapping("/view")
public class ViewController {

    @GetMapping("/page")
    public String greeting(){
        throw new RuntimeException("view page 요청!");
    }

    @GetMapping("/api")
    public @ResponseBody ResponseEntity<Map<String, String>> api(){
        throw new RuntimeException("view - api의 ResponseBody 요청!");
    }
}
  • ViewExceptionHandler가 동작하기를 바라는 엔드포인트
    • /view/page
    • /view/api
  • RestExceptionHandler가 동작하기를 바라는 엔드포인트
    • /rest/api
  • 하지만 모든 예외가 RestExceptionHandler로 처리되었다.

해소 : ControllerAdvice만 사용한다.

  • 다만 아래의 형태로 문제를 해결할 수 있었다.
@Order(Ordered.HIGHEST_PRECEDENCE) // Order가 없을 경우 ViewExceptionHandler만 동작하였다.
@ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
    // 생략
}
  • RestControllerAdvice와 ControllerAdvice가 왜 동시에 동작하는지는 확인하지 못했다. 다만 위의 형태를 사용하면 원하는 방향대로 동작함은 확인하였다.
  • 이러한 문제가 발생하는 이유를 아시는 분 있으면 공유 부탁드립니다! 🤞

위의 현상을 재현하기 위한 프로젝트와 테스트 코드를 작성하였습니다.

  • https://github.com/infoqoch/openstudy/tree/master/spring-exception-handler-scope-test