JiwonDev

Spring ์˜ˆ์™ธ์ฒ˜๋ฆฌ - API, @ExceptionHandler

by JiwonDev

2021.08.30 - [Backend/Spring MVC] - Spring ์˜ˆ์™ธ์ฒ˜๋ฆฌ - ์˜ค๋ฅ˜ํŽ˜์ด์ง€(404,500)

 

Spring ์˜ˆ์™ธ์ฒ˜๋ฆฌ - ์˜ค๋ฅ˜ํŽ˜์ด์ง€(404,500)

์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ๋Š” ์–ด๋–ค ๋ฐฉ๋ฒ•์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ๋ฐฐ์›Œ๋ณด๊ณ  Spring์—์„œ์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์ž. # ์„œ๋ธ”๋ฆฟ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์„œ๋ธ”๋ฆฟ์€ WAS์—์„œ ๋™์ž‘ํ•˜๋Š” ์ž๋ฐ” ๊ฐ์ฒด์ด๋‹ค. ์„œ๋ธ”๋ฆฟ์€ ๋‹ค์Œ 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•

jiwondev.tistory.com

 

๊ณ ๊ฐ์—๊ฒŒ ๋ณด์—ฌ์ค„ HTML ์˜ค๋ฅ˜ํŽ˜์ด์ง€๋Š” ์Šคํ”„๋ง๋ถ€ํŠธ์˜ BasicErrorController๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ–ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด JSON๋“ฑ์˜ API์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ๊นŒ?

 

 

# API์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ

๊ถ๊ธˆํ•˜๋‹ˆ๊นŒ ์ผ๋‹จ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋ฉด ์Šคํ”„๋ง์—์„œ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž.

api ๋ฐ˜ํ™˜๊ฐ’์— HTML ์˜ค๋ฅ˜ํŽ˜์ด์ง€๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค...์–ด? ์ด๋ž˜๋„ ๋˜๋‚˜?

@Slf4j
@RestController
public class ApiExceptionController {

  @GetMapping("/api/members/{id}")
  public MemberDto getMember(@PathVariable("id") String id) {

    if (id.equals("ex")) {
      throw new RuntimeException("์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž");
    }
    if (id.equals("bad")) {
      throw new IllegalArgumentException("์ž˜๋ชป๋œ ์ž…๋ ฅ ๊ฐ’");
    }
    if (id.equals("user-ex")) {
      throw new UserException("์‚ฌ์šฉ์ž ์˜ค๋ฅ˜");
    }

    return new MemberDto(id, "hello " + id);
  }
}

 


@ ์˜ค๋ฅ˜ํŽ˜์ด์ง€ ๋Œ€์‹  ์˜ค๋ฅ˜ API๋กœ ์ „ํ™˜ํ•˜๊ธฐ

API ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ–ˆ๋Š”๋ฐ HTML์„ ๋ณด๋‚ด๋ฒ„๋ฆฌ๋ฉด ์•ˆ๋œ๋‹ค. ๋‹คํ–‰ํžˆ๋„ ์Šคํ”„๋ง @RequestMapping ์–ด๋…ธํ…Œ์ด์…˜์—๋Š” HTP ํ—ค๋”๋ฅผ ๋ถ„์„ํ•ด JSON API ์š”์ฒญ์ด ์™”์„ ๋•Œ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

 โžก @RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)

// ErrorController ๋ฅผ ์ง์ ‘ ๊ตฌํ˜„ํ•œ ๊ฒฝ์šฐ

@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Map<String, Object>> errorPage500Api(
      HttpServletRequest request, HttpServletResponse response) {

    log.info("API errorPage 500");

    
    // RequestDispatcher.ERROR_EXCEPTION, ERROR_STATUS_CODE ์ƒ์ˆ˜
    Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
    
    Map<String, Object> result = new HashMap<>();
    result.put("status", request.getAttribute(ERROR_STATUS_CODE));
    result.put("message", ex.getMessage());

    Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
  }
}

 

๊ทธ๋Ÿฌ๋ฉด HTML ์˜ค๋ฅ˜ ํŽ˜์ด์ง€์™€ ๊ฐ™์€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ WAS๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋‹ค์‹œ ํ˜ธ์ถœํ•œ๋‹ค.

 

 


# ์Šคํ”„๋ง ๋ถ€ํŠธ์˜ BasicErrorController

์•ž์—์„œ ์—๋Ÿฌ์šฉ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์ง€์•Š์•„๋„, ์›น ํŽ˜์ด์ง€์—์„œ๋Š” BasicErrorController๋ฅผ ์ด์šฉํ•ด ๊ฐœ๋ฐœ์ž๋Š” error/404.html๋งŒ ์ž‘์„ฑํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณต ํ•  ์ˆ˜์žˆ์—ˆ๋‹ค. API๋Š” ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์„๊นŒ?

์Šคํ”„๋ง๋ถ€ํŠธ์—์„œ ์ƒ์„ฑํ•˜๋Š” BasicErrorContorller ๊ฐ์ฒด

 

  • ์‚ฌ์‹ค BasicErrorController๋Š” HTTP ์š”์ฒญ Header์— ์žˆ๋Š” Accept๋ฅผ ๋ณด๊ณ  ์˜ค๋ฅ˜ํŽ˜์ด์ง€๋ฅผ ์„ ํƒํ•ด์ค€๋‹ค.
    โžก Accept = text/html ๋ผ๋ฉด 404.html์„, Accept = application/json ์ด๋ผ๋ฉด Json ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

Accept ํ—ค๋”๊ฐ’์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋„๋ก ์ด๋ฏธ ์ •์˜๋˜์–ด์žˆ๋‹ค.
์•ž์—์„œ ํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ, ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ฒŒ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

  • ์ด๋Š” BasicErrorController ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.
    โžก text/html์ด๋ผ๋ฉด ModelAndView๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ๊ทธ ์™ธ์˜ ์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋Š” ResponeEntity<>๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

BasicErrorController์— ์ ํ˜€์žˆ๋Š” ์ฝ”๋“œ

 

 

@ BasicController ๋งŒ์œผ๋กœ๋Š” API ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์— ํ•œ๊ณ„๊ฐ€ ์žˆ๋‹ค.

HTML ์š”์ฒญ์˜ ์˜ค๋ฅ˜ํŽ˜์ด์ง€๋Š” ๋‹จ์ˆœํ•˜๊ฒŒ 404, 500 ์ฝ”๋“œ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์˜ค๋ฅ˜ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ฃผ๋ฉด ๋˜์—ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ API๋Š” ์ •๋ง ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๊ฐ€ ๋‚˜์˜จ๋‹ค. ํด๋ผ์ด์–ธํŠธ, ์„œ๋ฒ„๋งˆ๋‹ค API ์ŠคํŽ™์ด ๋‹ค๋ฅผ์ˆ˜๋„์žˆ๊ณ , ๊ฐ™๋‹ค๊ณ  ํ•˜๋”๋ผ๋„ [Item API, Order API]๋“ฑ ์š”์ฒญ๋งˆ๋‹ค ๋‹ค๋ฅธ ์˜ค๋ฅ˜ JSON ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์„๊นŒ?

 


# API ์˜ˆ์™ธ์ฒ˜๋ฆฌ - HandlerExceptionResolver

์Šคํ”„๋ง MVC๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฐ–์œผ๋กœ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, ์ด ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” HandlerExceptionResolver ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค. ๋งŒ์•ฝ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ WAS๋กœ ๋˜์ง€์ง€ ์•Š๊ณ  ์ค‘๊ฐ„์— ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

 โžก  ExceptionResolver๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ตณ์ด WAS๋ฅผ ๊ฑฐ์ณ์„œ ๋Œ์•„์˜ฌ ํ•„์š”๊ฐ€ ์—†๋‹ค. ๊ทธ๋ƒฅ ์˜ˆ์™ธ๋ฅผ try-catch์ฒ˜๋Ÿผ ์žก์œผ๋ฉด ๋œ๋‹ค.

์ฐธ๊ณ ๋กœ ExceptionResolver๋ฅผ ์‚ฌ์šฉํ•ด๋„ ์˜ˆ์™ธ๊ฐ€ ํ„ฐ์ง€๋ฉด ์ธํ„ฐ์…‰ํ„ฐ์˜ postHandle(...)๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๊ฑด ๋ณ€ํ•จ์—†๋‹ค.

 

  • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ์ค‘๊ฐ„์— ๋‚š์•„์ฑ„์„œ ์ง€์ •ํ•œ ๋ฐ˜ํ™˜๊ฐ’์ด ๋‚˜์˜ค๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
    โžก ExceptionResolver๋Š” ์—ฌ๋Ÿฌ๊ฐœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰ null์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋‹ค์Œ ๋ฆฌ์กธ๋ฒ„๋กœ ๋„˜๊ธด๋‹ค.
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex) {

    try {
      if (ex instanceof IllegalArgumentException) {
        // API์˜ ๊ฒฝ์šฐ ์ด๋ ‡๊ฒŒ ์‘๋‹ต๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Œ.
        // ์ง์ ‘ ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜๊ณ ์‹ถ๋‹ค๋ฉด => response.getWriter().println("hello"); 
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        
        // View๊ฐ€ ํ•„์š”์—†๋Š” ๊ฒฝ์šฐ, ๊ทธ๋ƒฅ ๋น„์–ด์žˆ๋Š” ModelAndView๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ทฐ๊ฐ€ ๋ Œ๋”๋ง ๋˜์ง€์•Š์Œ.
        return new ModelAndView();
      }
    } catch (IOException e) {
       // response.writer ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ์ฒ˜๋ฆฌ
    }

    return null; // ๋‹จ ํ•ด๋‹น ์˜ˆ์™ธ(Exception ex)๋ฅผ ์ฒ˜๋ฆฌํ•  ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์—†๋‹ค๋ฉด WAS๋กœ ๋„˜๊น€.
  }
}

 

์ด๋ ‡๊ฒŒ ๋งŒ๋“  ํ•ธ๋“ค๋Ÿฌ๋Š” ํ•„ํ„ฐ, ์ธํ„ฐ์…‰ํ„ฐ์ฒ˜๋Ÿผ @Configuration์—์„œ ๋“ฑ๋กํ•˜๋ฉด ๋œ๋‹ค.

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    // configureHandler...๋ฉ”์„œ๋“œ๋Š” ๊ธฐ์กด์˜ ์Šคํ”„๋ง ์˜ˆ์™ธํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ญ์ œํ•ด๋ฒ„๋ฆฐ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ž.
    resolvers.add(new MyHandlerExceptionResolver());
    resolvers.add(new UserHandlerExceptionResolver());
  }


  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor())
        .order(1)
        .addPathPatterns("/**")
        .excludePathPatterns("/css/**", "*.ico", "/error/**", "/error-page/**");
  }
}

 

 


# HandlerExceptionResolver ํ™œ์šฉ๋ฐฉ๋ฒ•

  • ์˜ˆ์™ธ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผํ•˜๋ฏ€๋กœ ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉํ•  Exception์„ ์ •์˜ํ•œ๋‹ค.
public class UserException extends RuntimeException {
  /* RuntimeException์˜ ์ƒ์„ฑ์ž๋งŒ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ์ฝ”๋“œ๋Š” ์ƒ๋žต */
}

 

  • ํ•ด๋‹น ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•  ExceptionResolver๋ฅผ ๋งŒ๋“ค๊ณ , @Configuration ๊ฐ์ฒด์— ๋ฆฌ์กธ๋ฒ„๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new UserHandlerExceptionResolver());
  }
}
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex) {

    try {
      // ๋‚ด๊ฐ€ ๋งŒ๋“  ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ
      if (ex instanceof UserException) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 ์—๋Ÿฌ๋กœ ์ง€์ •
        String acceptHeader = request.getHeader("accept");

        if ("application/json".equals(acceptHeader)) { // JSON ์‘๋‹ต์ธ ๊ฒฝ์šฐ
          Map<String, Object> errorResult = new HashMap<>();
          errorResult.put("ex", ex.getClass());
          errorResult.put("message", ex.getMessage());

          // ๊ฐ์ฒด๋ฅผ ๋ฌธ์ž์—ด(JSON)์œผ๋กœ ๋ณ€๊ฒฝํ•จ.
          String result = objectMapper.writeValueAsString(errorResult);
          
          // response body ๋ฅผ ์ž‘์„ฑํ•จ.
          response.setContentType("application/json");
          response.setCharacterEncoding("utf-8");
          response.getWriter().write(result);
          return new ModelAndView();

        } else { // ๊ทธ๋ƒฅ HTML ์‘๋‹ต์„ ์š”๊ตฌํ•œ ๊ฒฝ์šฐ
          return new ModelAndView("error/500");
        }
      }

    } catch (IOException e) {
      // response.writer ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ์ฒ˜๋ฆฌ
    }

    return null; // ๊ทธ ์™ธ ์—๋Ÿฌ๋Š” ๋‹ค์Œ ๋ฆฌ์กธ๋ฒ„(์—†๋‹ค๋ฉด WAS)์— ๋„˜๊น€.
  }
}

 

์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š”๊ฑด ์ข‹์ง€๋งŒ ๋งค๋ฒˆ ๋ฆฌ์กธ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ๋ฒˆ๊ฑฐ๋กญ๋‹ค. ๋” ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์—†์„๊นŒ?

 

 


# ์Šคํ”„๋ง๋ถ€ํŠธ์˜ ExceptionResolver

์—ญ์‹œ๋‚˜ ์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ๋Š” ์‚ฌ์šฉํ•˜๊ธฐ ํŽธํ•˜๊ฒŒ ์•„๋ž˜์˜ ExceptionResolver๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘๊ณ  ์ž๋™์œผ๋กœ ๋“ฑ๋กํ•ด์ค€๋‹ค.

์•„๋ž˜ 3๊ฐœ์˜ ๊ธฐ๋ณธ ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค. ํ˜„์žฌ ๋ฆฌ์กธ๋ฒ„์—์„œ null์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ ๋‹ค์Œ ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

  • 1. ExceptionHandlerExceptionResolver
    โžก ์Šคํ”„๋ง์˜ @ExceptionHandler๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฆฌ์กธ๋ฒ„์ด๋‹ค. ์Šคํ”„๋ง API ์˜ˆ์™ธ๋Š” ๋Œ€๋ถ€๋ถ„ ์ด๊ฑธ ์‚ฌ์šฉํ•œ๋‹ค.
  • 2. ResponseStatusExceptionResolver
    โžก @ResponseStatus(HttpStatus.NOT_FOUND) ๊ฐ™์€ ๊ฑธ ์ฝ์–ด ์˜ˆ์™ธ์— ๋”ฐ๋ผ HTTP ์ƒํƒœ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฆฌ์กธ๋ฒ„์ด๋‹ค.
    โžก ์ฐธ๊ณ ๋กœ reason์— ์Šคํ”„๋ง์˜ ๋ฉ”์‹œ์ง€("error.bad")๋ฅผ ์ด์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๋“ฑ๋ก๋œ๊ฒŒ ์—†๋‹ค๋ฉด ๊ทธ๋ƒฅ ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

@ResponseStatus ํƒœ๊ทธ๋ฅผ ์ด์šฉํ•ด์„œ ํ•ด๋‹น ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ๋ฐ˜ํ™˜ํ•  HTTP ์ƒํƒœ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

  • 3. DefaultHandlerExceptionResolver ( ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ€์žฅ ๋‚ฎ์€ ๋ฆฌ์กธ๋ฒ„ )
    โžก ๊ทธ ์™ธ ์Šคํ”„๋ง ๋‚ด๋ถ€์— ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ๋“ค์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ธฐ๋ณธ ๋ฆฌ์กธ๋ฒ„์ด๋‹ค.
    โžก ์˜ˆ) ์š”์ฒญ ๋ฉ”์‹œ์ง€ ์ž๋ฃŒํ˜•์ด ์ด์ƒํ•ด์„œ(TypeMismatchException) ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ HTTP 400์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. 

- @ResponseStatus(...)์˜ ๊ฒฝ์šฐ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•ด์•ผ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค. 

- ์ฐธ๊ณ ๋กœ @ResponseStatus(...)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๊ธฐ์กด ์˜ˆ์™ธ๋ฅผ ResponseStatusException์œผ๋กœ ๊ฐ์‹ธ์„œ ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋งŒ๋“ ๋‹ค. ์ฆ‰ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ throw Resp...Exception์„ ๋˜์ ธ๋„ ํ•ด๋‹น ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.

 

 


# API ์ฒ˜๋ฆฌ์™€ @ExceptionHandler

ํ•œ๋ฒˆ ๋” ๋ณต์Šตํ•˜์ž๋ฉด, HTML ํ™”๋ฉด ์˜ค๋ฅ˜๋Š” ๊ทธ๋ƒฅ BasicErrorController๋ฅผ ์ด์šฉํ•ด์„œ 404 ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด ํ•ด๊ฒฐ๋œ๋‹ค.

ํ•˜์ง€๋งŒ API์˜ ๊ฒฝ์šฐ์—๋Š” ๋‹ค๋ฅด๋‹ค. Item-API, Order-API๋“ฑ ์„œ๋น„์Šค ์ข…๋ฅ˜์— ๋”ฐ๋ผ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ค˜์•ผํ•˜๊ณ  ์‹ฌ์ง€์–ด๋Š” ํด๋ผ์ด์–ธํŠธ์— ๋”ฐ๋ผ API์˜ ์ŠคํŽ™ ์ž์ฒด๊ฐ€ ๋‹ฌ๋ผ์ง€๊ธฐ๋„ ํ•œ๋‹ค. ๋ฌผ๋ก  ํ•„์š”ํ•˜๋‹ค๋ฉด HTML ํ™”๋ฉด๋„ ๋ฆฌ์กธ๋ฒ„๋กœ ์ฒ˜๋ฆฌํ•ด๋„ ๋œ๋‹ค.

 

โžก ๊ทธ๋ž˜์„œ API์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋Š” ๋ณดํ†ต ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•œ ExceptionHandlerExceptionResolver๋ฅผ ์ด์šฉํ•œ๋‹ค.
โžก ๋ฌผ๋ก  ๋ฆฌ์กธ๋ฒ„๋Š” ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๋‹ค๋ฅธ ExceptionResolver๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

@ExceptionHandler ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ

ExceptionResolver๋Š” ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด, ์ปจํŠธ๋กค๋Ÿฌ ์•ˆ์— @ExceptionHandler ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์˜ˆ์™ธ๋ฅผ ํ•ด๋‹น ๋ฉ”์„œ๋“œ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋งŒ๋“ค์–ด์ฃผ๊ณ  HTTP ์š”์ฒญ์— ์ •์ƒ ์‘๋‹ต(200)ํ•œ๋‹ค.

โžก ๋งŒ์•ฝ HTTP ์˜ค๋ฅ˜์ฝ”๋“œ๋ฅผ 400์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์— @ResponseStatus๋ฅผ ์ถ”๊ฐ€๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

@Slf4j
@RestController
public class ApiExceptionV2Controller {
  
  // ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” Mapping ๋ฉ”์„œ๋“œ
  @GetMapping("/api2/members/{id}")
  public MemberDto getMember(@PathVariable("id") String id) {
    if (id.equals("ex")) { throw new RuntimeException("์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž");}
    if (id.equals("bad")) { throw new IllegalArgumentException("์ž˜๋ชป๋œ ์ž…๋ ฅ ๊ฐ’");}
    if (id.equals("user-ex")) { throw new UserException("์‚ฌ์šฉ์ž ์˜ค๋ฅ˜");}
    
    return new MemberDto(id, "hello " + id);
  }
  
  
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandle(IllegalArgumentException e) {
    return new ErrorResult("BAD", e.getMessage());
  }


  @ExceptionHandler
  public ResponseEntity<ErrorResult> userExHandle(UserException e) {
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
  }
}

 


@ExceptionHandler์˜ ์ถ”์ƒํ™”

๋†€๋ž๊ฒŒ๋„ ์Šคํ”„๋ง์˜ @ExceptionHandler๋Š” SpringMVC ์ปจํŠธ๋กค๋Ÿฌ์ฒ˜๋Ÿผ ์ถ”์ƒํ™”๋˜์–ด์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์•„๋ž˜์˜ ์˜ˆ์‹œ๋“ค์ฒ˜๋Ÿผ ํŒŒ๋ผ๋ฉ”ํƒ€, ๋ฆฌํ„ดํƒ€์ž…์„ ๋‹ค์–‘ํ•˜๊ฒŒ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ์•„๋งˆ ์˜ˆ์ƒํ–ˆ๊ฒ ์ง€๋งŒ, DispatcherServlet์—์„œ @ExceptionHandler ๋˜ํ•œ ๋ฆฌ์กธ๋ฒ„๋“ค์„ ์ด์šฉํ•ด ์ถ”์ƒํ™”ํ•œ๋‹ค.

 

  • ์œ„์˜ ์˜ˆ์ œ์ฒ˜๋Ÿผ ํŒŒ๋ผ๋ฉ”ํƒ€์˜ ํƒ€์ž…์ด ์˜ˆ์™ธ ํƒ€์ž…๊ณผ ๊ฐ™๋‹ค๋ฉด @ExceptionHandler( ์˜ˆ์™ธ.class ) ๋ฅผ ์ƒ๋žตํ•ด๋„ ๋œ๋‹ค.
  @ExceptionHandler // @ExceptionHandler(UserException.class)์™€ ๊ฐ™์€ ๋™์ž‘ 
  public ResponseEntity<ErrorResult> userExHandle(UserException e) {
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
  }

 

  • ExceptionResolver๋Š” ํ•ด๋‹น ์˜ˆ์™ธ์˜ ์ž์‹ ๊ฐ์ฒด๊นŒ์ง€ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ฆ‰ ์•„๋ž˜์™€ ๊ฐ™์€ default ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
    โžก ๋‹ค๋งŒ @ExceptionHandler๊ฐ€ [๋ถ€๋ชจ, ์ž์‹] ์˜ˆ์™ธ๊ฐ€ ๋‹ค ์žˆ๋‹ค๋ฉด ๋” ๋””ํ…Œ์ผํ•œ ๊ฐ์ฒด, ์ฆ‰ ์ž์‹ ์˜ˆ์™ธ๋ถ€ํ„ฐ ๋จผ์ € ๋งคํ•‘ํ•œ๋‹ค.
  // ๋ฉ”์„œ๋“œ ์„ ์–ธ ์ˆœ์„œ๋Š” ์ƒ๊ด€์—†๋‹ค. ๋” ์ž์„ธํ•œ(์ž์‹) ์˜ˆ์™ธ๋ถ€ํ„ฐ ๋ฉ”์„œ๋“œ๊ฐ€ ๋งคํ•‘๋œ๋‹ค.
  // ๋ชจ๋“  Exception์„ ๋ฐ›๋Š” ์˜ˆ์™ธ์ฒ˜๋ฆฌ. ์‹ค์ˆ˜๋กœ ๋†“์น˜๋Š” ์˜ˆ์™ธ ๋ฐœ์ƒ์„ ๋ฐฉ์ง€ํ•ด์ค€๋‹ค.
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler
  public ErrorResult exHandle(Exception e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("EX", "๋‚ด๋ถ€ ์˜ค๋ฅ˜");
  }

 

  • ๋ฌผ๋ก  ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์˜ˆ์™ธ๋ฅผ ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ์— ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
@ExceptionHandler( {AException.class, BException.class} )
public String ex(Exception e) {
  log.info("exception e", e);
}

 

  • ์œ„์—๋„ ์–ธ๊ธ‰ํ–ˆ์ง€๋งŒ, ๊ผญ API์—๋งŒ ์‚ฌ์šฉํ•˜๋ผ๋Š” ๋ฒ•์€ ์—†๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด View๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค˜๋„ ๋œ๋‹ค.
    โžก ๋ฌผ๋ก  @ResponseBody๋‚˜ @RestController ํƒœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ๋ทฐ ๋ฆฌ์กธ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜์ง€์•Š์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
// ๋‹ค๋งŒ API ์ปจํŠธ๋กค๋Ÿฌ์— @RestController๋ฅผ ๋ถ™์ด๋ฏ€๋กœ, ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•์ด๊ธดํ•˜๋‹ค.
@ExceptionHandler(ViewException.class)
public ModelAndView ex(ViewException e) {
    log.info("exception e", e);
    return new ModelAndView("error"); // ๋ทฐ ๋ฆฌ์กธ๋ฒ„ ์‹คํ–‰๋จ
}

 


# @ControllerAdvice๋ฅผ ์ด์šฉํ•œ API ์˜ˆ์™ธ์ฒ˜๋ฆฌ

@ExceptionHandler๋Š” ๋„ˆ๋ฌด๋‚˜ ํŽธ๋ฆฌํ•œ ๊ธฐ๋Šฅ์ด์ง€๋งŒ, ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ ๊ฐ์ฒด์—๋งŒ ์ ์šฉ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

์ฆ‰ ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค @ExceptionHandler๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ์ž‘์„ฑํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ, ์ด๋ฅผ ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ ๊ณตํ†ต์œผ๋กœ ๋ฌถ์„ ์ˆœ ์—†์„๊นŒ?

 

โžก Spring AOP์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” @ControllerAdvice, @RestControllerAdvice ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋œ๋‹ค.

* ์ฐธ๊ณ ๋กœ @RestControllerAdvice๋Š” ๊ทธ๋ƒฅ ๊ธฐ์กดํƒœ๊ทธ์— @ResponseBody๊ฐ€ ์ถ”๊ฐ€๋กœ ๋ถ™์–ด์žˆ๋Š” ํƒœ๊ทธ์ด๋‹ค. ์ฆ‰ return์„ ํ–ˆ์„ ๋•Œ ๋ทฐ๋ฆฌ์กธ๋ฒ„๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

 

๐Ÿ“‘ ErrorResult  ๐Ÿ“‘ ExControllerAdvice โžก  @ExceptionHandler ๋ถ€๋ถ„์˜ ์ฝ”๋“œ๋ฅผ ๋ถ„๋ฆฌํ•ด ๋”ฐ๋กœ ์ž‘์„ฑํ•œ๋‹ค.

@Data
@AllArgsConstructor
public class ErrorResult {
    private String code;
    private String message;
}
@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public ErrorResult illegalExHandler(IllegalArgumentException e) {
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("BAD", e.getMessage());
  }

  @ExceptionHandler
  public ResponseEntity<ErrorResult> userExHandler(UserException e) {
    log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler
  public ErrorResult exHandler(Exception e) {
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("EX", "๋‚ด๋ถ€ ์˜ค๋ฅ˜");
  }

}

๊ทธ๋ƒฅ AOP๋ณด๋‹ค ์‚ฌ์šฉ๋ฒ•์ด ๋” ๊ฐ„๋‹จํ•˜๋‹ค. JoinPoint ๊ฐ™์€ ๊ฑฐ ํ•„์š”์—†์ด @ControllerAdvice๋งŒ ๋ถ™์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.

  • (basePackages ="...")๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ํ•ด๋‹น ํŒจํ‚ค์ง€์•ˆ์— ์žˆ๋Š” ๋ชจ๋“  @Controller์— ์ ์šฉ๋œ๋‹ค.
  • ๋”ฐ๋กœ ์ ์ง€์•Š๋Š”๋‹ค๋ฉด ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ์— ์žˆ๋Š” ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์— ์ ์šฉ๋œ๋‹ค. (Spring ๊ณต์‹๋ฌธ์„œ)

 

์ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด์ œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์˜ˆ์™ธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ๊ฐ์ฒด์˜ ์ฑ…์ž„์ด ๋ถ„๋ฆฌ๋˜์—ˆ๋‹ค. 

@Slf4j
@RestController
public class ApiExceptionV3Controller {

  @GetMapping("/api3/members/{id}")
  public MemberDto getMember(@PathVariable("id") String id) {

    if (id.equals("ex")) {
      throw new RuntimeException("์ž˜๋ชป๋œ ์‚ฌ์šฉ์ž");
    }
    if (id.equals("bad")) {
      throw new IllegalArgumentException("์ž˜๋ชป๋œ ์ž…๋ ฅ ๊ฐ’");
    }
    if (id.equals("user-ex")) {
      throw new UserException("์‚ฌ์šฉ์ž ์˜ค๋ฅ˜");
    }

    return new MemberDto(id, "hello " + id);
  }
}


// ์ฐธ๊ณ ๋กœ MemberDto๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฒผ๋‹ค.
@Data
@AllArgsConstructor
class MemberDto {

  private String memberId;
  private String name;
}

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ