์ด์ ๊น์ง์ ์์ธ ์ฒ๋ฆฌ ํฌ์คํ ์์๋ API ๋ฐฉ์์ด ์๋ ๊ธฐ๋ณธ์ ์ธ ํ๋ฉด์ ๋ฐํํ๋ ๋ฐฉ์์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๋ค๋ฃจ์๋ค.
๊ทธ๋ฌ๋ API ํต์ ์์ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, ํ๋ฉด์ ๋ฐํํ๋ ๊ฒ์ ์๋ฌด๋ฐ ์ธ๋ชจ๊ฐ ์๋ค.
๋ฐ๋ผ์ API ํต์ ์์ ์์ธ๊ฐ ๋ฐ์ํ์ ๊ฒฝ์ฐ์๋ ์ด๋ค ์์ผ๋ก ์์ธ ๋ฉ์ธ์ง๋ฅผ ๋ฐํํ ๊ฒ์ธ์ง๋ฅผ ์ ํ๊ณ , ๊ทธ์ ๋ง๊ฒ json์ผ๋ก ์์ธ ๋ฉ์ธ์ง๋ฅผ ๋ฐํํด ์ฃผ์ด์ผ ํ๋ค.
์คํ๋ง ๋ถํธ๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ
์คํ๋ง ๋ถํธ๋ BasicErrorController๋ฅผ ํตํด ๊ธฐ๋ณธ์ ์ธ ์์ธ์ฒ๋ฆฌ๋ฅผ ์ํํ๋ค๊ณ ํ์๋ค.
API ์์ธ ๋ํ ์ด๊ณณ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฒ๋ฆฌํด ์ฃผ๋๋ฐ, ๋ฐํํด์ฃผ๋ json์ ๋ค์๊ณผ ๊ฐ์ ํ์์ด๋ค.
Accept ํค๋๊ฐ application/json์ธ ๊ฒฝ์ฐ
{
"timestamp": "2022-01-07T13:17:21/064+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"path": "api/member/ex",
}
๊ทธ๋ฌ๋ API ์ค๋ฅ ์ฒ๋ฆฌ๋ ์์ ๊ฐ์ ๊ธฐ๋ณธ์ ์ธ ์์ธ ๋ฉ์ธ์ง๋ก๋ ๋ถ์กฑํ๋ค. ๊ฐ๊ฐ์ ์ปจํธ๋กค๋ฌ๋ ์์ฒญ๋ง๋ค ์์ธํ ์์ธ ๋ฉ์ธ์ง๋ฅผ ๋ณด์ฌ์ฃผ์ด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
์ด์ ์ด์๊ฐ์ API ์์ธ ๋ฉ์ธ์ง๋ฅผ ์ปค์คํ ํ์ฌ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด์.
HandlerExceptionResolver
์์ธ๊ฐ ๋ฐ์ํด์ ์๋ธ๋ฆฟ์ ๋์ด WAS๊น์ง ์ ๋ฌ๋๋ค๋ฉด, ์ด์ ์ ์์๋ดค๋ ๊ฒ๊ณผ ๊ฐ์ด HTTP ์ํ์ฝ๋ 500์ผ๋ก ์ฒ๋ฆฌ๋๋ค.
๋ฐ์ํ๋ ์์ธ์ ๋ฐ๋ผ์ ์ํ์ฝ๋๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ถ์ ๊ฒฝ์ฐ๊ฐ ์์ ๊ฒ์ด๋ค.
์คํ๋ง์ ์ปจํธ๋กค๋ฌ ๋ฐ์ผ๋ก ์์ธ๊ฐ ๋์ ธ์ง ๊ฒฝ์ฐ ํด๋น ์์ธ๋ฅผ ํด๊ฒฐํ๊ณ ๋์์ ์๋กญ๊ฒ ์ ์ํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํ๋ค.
์ด๋ฐ ๊ฒฝ์ฐ์ ์ฌ์ฉํ ์ ์๋ ๊ฒ์ด ๋ฐ๋ก HandlerExceptionResolver์ด๋ค. ์ค์ฌ์ ExceptionResolver๋ผ๊ณ ํ๋ค.
ExceptionResolver๊ฐ ์ ์ฉ๋๊ธฐ ์ ์๋, ์ปจํธ๋กค๋ฌ์์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด, WAS๊น์ง ์์ธ๊ฐ ์ ๋ฌ๋์ด 500์ ๋ฐํํ๋ค.
๊ทธ๋ฌ๋ ExceptionResolver๊ฐ ์ ์ฉ๋๋ค๋ฉด, ์ปจํธ๋กค๋ฌ์์ ๋ฐ์ํ ์์ธ๋ฅผ ExceptionResolver๊ฐ ์ฒ๋ฆฌํ ํ, WAS์๋ ์ ์ ์๋ต์ผ๋ก ๋์ด๊ฐ๊ฒ ๋๋ค.
(์ฐธ๊ณ ๋ก ExceptionResolver๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ค๊ณ ํ๋๋ผ๋ ์ธํฐ์ ํฐ์ postHandle์ ์๋ํ์ง ์์ผ๋ ์ฃผ์ํ์)
์ฌ์ฉ
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
์ ์์์์๋ IllegalArgumentException์ด ๋ฐ์ํ๋ค๋ฉด, ์ด๋ฅผ HTTP ์ํ์ฝ๋ 400์ผ๋ก ์ฒ๋ฆฌํ๋ ์์์ด๋ค.
๋น ModelAndView๋ฅผ ๋ฐํํ ์ด์ ?
๋น ModelAndView๋ฅผ ๋ฐํํ๋ฉด ๋ทฐ๋ฅผ ๋ ๋๋ง ํ์ง ์์ผ๋ฉด์ ์ ์ ํ๋ฆ์ผ๋ก ์๋ธ๋ฆฟ์ด ๋ฆฌํด๋๋ค.
์ด์ ์ด๋ฅผ ๋ฑ๋กํด๋ณด์.
WebMvcConfigurer๋ฅผ ํตํด ๋ฑ๋ก
@Configuration
public class WebConfig implements WebConfigurer{
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
์คํ๋ง์ด ์ ๊ณตํ๋ ExceptionResolver
์ฐ๋ฆฌ๋ ์์์ ExceptionResolver๋ฅผ ์ง์ ๊ตฌํํด๋ณด์๋ค.
์คํ๋ง ๋ถํธ๋ ์์ธ์ฒ๋ฆฌ๋ฅผ ์ข ๋ ํธํ๊ฒ ํ ์ ์๋๋ก ๊ธฐ๋ณธ์ผ๋ก ๋ช๊ฐ์ง ExceptionResolver๋ฅผ ์ ๊ณตํ๋๋ฐ ๋ค์๊ณผ ๊ฐ๋ค.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver -> ์ฐ์ ์์๊ฐ ๊ฐ์ฅ ๋ฎ๋ค
ExceptionHandlerExeptionResolver
@ExceptionHandler๋ฅผ ์ฒ๋ฆฌํ๋ค. API ์์ธ ์ฒ๋ฆฌ๋ ๋๋ถ๋ถ ์ด๊ฒ์ผ๋ก ํด๊ฒฐํ๋ค.
ResponseStatusExceptionResolver
HTTP ์ํ ์ฝ๋๋ฅผ ์ง์ ํด์ค๋ค.
(์: @ResponseStatus(value = HttpStatus.NOT_FOUND))
DefaultHandlerExceptionResolver
์คํ๋ง ๋ด๋ถ ๊ธฐ๋ณธ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ค.
ResponseStatusExceptionResolver
๊ฐ์ฅ ์ฌ์ด ResponseStatusExceptionResolver๋ถํฐ ์์๋ณด๋๋ก ํ์.
ResponseStatusExceptionResolver๋ ๋ค์ ๋ ๊ฐ์ง ๊ฒฝ์ฐ์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ค.
- @ResponseStatus๊ฐ ๋ฌ๋ ค์๋ ์์ธ
- ResponseStatusException ์์ธ
//@ResponseStatus๊ฐ ๋ฌ๋ ค์๋ ์์ธ
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "์๋ชป๋ ์์ฒญ ์ค๋ฅ")
public class BadRequestException extends RuntimeException {}
//ResponseStatusException ์์ธ
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver๋ ์คํ๋ง ๋ด๋ถ์์ ๋ฐ์ํ๋ ์คํ๋ง ์์ธ๋ฅผ ํด๊ฒฐํ๋ค.
์๋ฅผ ๋ค์ด ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์์ ์ ํ์ ์ด ๋ง์ง ์์ผ๋ฉด ๋ด๋ถ์์ TypeMismatchException์ด ๋ฐ์ํ๋๋ฐ, ์ด ๊ฒฝ์ฐ์๋ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๊ทธ๋ฅ ๋๋ฉด ์๋ธ๋ฆฟ ์ปจํ ์ด๋๊น์ง ์ค๋ฅ๊ฐ ์ ๋ฌ๋๊ณ , ๊ฒฐ๊ณผ์ ์ผ๋ก 500์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
๊ทธ๋ฌ๋ ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ์ ๋๋ถ๋ถ ํด๋ผ์ด์ธํธ์ ์ค์์ด๋ฏ๋ก DefaultHandlerExceptionResolver์์๋ ์ด๊ฒ์ 500์ค๋ฅ๊ฐ ์๋๋ผ HTTP ์ํ์ฝ๋ 400์ค๋ฅ๋ก ๋ณ๊ฒฝํ๋ค.
์๋ ํ๋์ ์์์๊ณ , ์์ ์์ ๋ง๊ณ ๋ ์ฌ๋ฌ ์์ธ๋ฅผ ๋ณํํด์ฃผ๋, ๊ถ๊ธํ๋ค๋ฉด ํ๋ฒ ํ์ธํด ๋ณด๋ ๊ฒ๋ ์ข์ ๊ฒ ๊ฐ๋ค.
@ExceptionHandler
์คํ๋ง์ API์์ธ ์ฒ๋ฆฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ @ExceptionHandler๋ผ๋ ์ ๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ ์์ธ ์ฒ๋ฆฌ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ์ด๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ๋ฐ๋ก ExceptionHandlerExceptionResolver์ด๋ค.
์ฌ์ฉ ์์๋ฅผ ๋ณด๋ฉฐ ์์๋ณด์.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] 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 exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "๋ด๋ถ ์ค๋ฅ");
}
@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);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
์ ์์์ฒ๋ผ @ExceptionHandler์๋ ํด๋น ์ปจํธ๋กค๋ฌ์์ ์ฒ๋ฆฌํ๊ณ ์ถ์ ์์ธ๋ฅผ ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
ํด๋น ์ปจํธ๋กค๋ฌ์์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์ด ๋ฉ์๋๊ฐ ํธ์ถ๋๋ค.
์ง์ ํ ์์ธ์, ๊ทธ ํ์ ์์ธ๋ ๋ชจ๋ ์ก์ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ถ๋ชจ ์์ธ์ ์์ ์์ธ๋ฅผ ๋ชจ๋ ์ง์ ํ๋ค๋ฉด, ๋น์ฐํ ์์ ์์ธ๋ฅผ ์ฐ์ ์์๊ฐ ๋๊ฒ ์ฒ๋ฆฌํ๋ค.
์๋์ ๊ฐ์ด ๋ค์ํ ์์ธ๋ฅผ ์ก๊ฑฐ๋, ์์ธ๋ฅผ ์๋ตํ ์๋ ์๋ค.
(์์ธ๋ฅผ ์๋ตํ๋ฉด ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ์์ธ๊ฐ ์ง์ ๋๋ค.)
@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
log.info("exception e", e);
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {}
์ค์ํ ์ ์ @ExceptionHandler๋ ํ๋์ ์ปจํธ๋กค๋ฌ ๋ด๋ถ์์ ๋ฐ์ํ๋ ์์ธ๋ง ์ฒ๋ฆฌํ๋ค๋ ๊ฒ์ด๋ค.
๊ทธ๋ผ ์ ์ ์ฝ๋์ ์์ธ ์ฒ๋ฆฌ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด ์์๊น?
๊ทธ๋ด ๋ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ฐ๋ก @ControllerAdvice ํน์ @RestControllerAdvice์ด๋ค.
@ControllerAdvice
@ControllerAdvice ๋ ๋์์ผ๋ก ์ง์ ํ ์ฌ๋ฌ ์ปจํธ๋กค๋ฌ์ @ExceptionHandler , @InitBinder ๊ธฐ๋ฅ์
๋ถ์ฌํด์ฃผ๋ ์ญํ ์ ํ๋ค.
@ControllerAdvice ์ ๋์์ ์ง์ ํ์ง ์์ผ๋ฉด ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ์ ์ฉ๋๋ค. (๊ธ๋ก๋ฒ ์ ์ฉ)
์ ์ฉ๋ฐฉ๋ฒ
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,AbstractController.class})
public class ExampleAdvice3 {}