-
Spring Boot ๊ณตํต Global Exception HandlerSpring 2022. 10. 23. 17:46๋ฐ์ํ
๐ Spring์์ ์ ์ญ์ ์ผ๋ก Exception์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ํฌ์คํ ํ๋ค.
์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๋ค์ํ๊ฒ ์๋ค.
1. ๋ฉ์๋ ๋ด ์์ธ ์ํฉ์ ์์ธกํ์ฌ try-catch๋ฌธ ์ฌ์ฉ
2. ์๊ตฌ์ฌํญ์ ๋ํ ์์ธ ์ฒ๋ฆฌ (validation)
3. Intercepter์์ ์ ์์ธ ์ฒ๋ฆฌ
4. HandlerExceptionResolver
5. ExceptionHandlerExceptionResolver
6. DefaultHandlerExceptionResolver
7. ResponseStatusExceptionResolver์์ ๊ฐ์ ๋ฐฉ๋ฒ๋ค ๋ง๊ณ ๋ ๋ค์ํ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๋ค.
ํ์ง๋ง ๊ฐ๊ฐ์ ์๋ฌ์ ์ผ์ผ์ด ์ฒ๋ฆฌํ๋ค๋ณด๋ฉด,
๋น์ฆ๋์ค ๋ก์ง์ ์ง์ค์ ํ ์ ์๊ณ , ์ฝ๋๊ฐ ๋ณต์กํด์ง๋ ์ํฉ์ด ๋ฐ์ํ๋ค.๊ถ๊ทน์ ์ผ๋ก, ๊ณตํต์ ์ผ๋ก ๋ฐ์ํ๋ ์๋ฌ๊ฐ ์๋ค๋ฉด ๊ณตํต์ ์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๋๊ฒ ํจ์จ์ ์ธ ๋ฐฉ์์ด๋ค.
๊ทธ ๋ฐฉ๋ฒ์ผ๋ก, @Controller์ @ControllerAdvice, @ExceptionHandler๋ฅผ
์ด์ฉํ ์ปจํธ๋กค๋ฌ๋จ์์ ์ ์ญ์ ์ผ๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํ๋ค.
๐ @ExceptionHandler
- ํด๋น ์ด๋ ธํ ์ด์ ์ @Controller, @RestController๊ฐ ์ ์ฉ๋ Bean๋ด์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ ์ก์์ ํ๋์ ๋ฉ์๋์์ ์ฒ๋ฆฌํด์ฃผ๋ ๊ธฐ๋ฅ์ ํ๋ค.
- ์๋์ ์์์ฒ๋ผ ํด๋น ๋ฉ์๋์ ์ด๋ ธํ ์ด์ ๊ณผ Exception Class๋ฅผ ์ ์ธํด์ฃผ๋ฉด ํด๋น Exception์ด ๋ฐ์ํ์๋ ์ํ๋ ๋ก์ง์ ์ฒ๋ฆฌํ ์ ์๋ค.
- TestController์ ํด๋นํ๋ Bean์์ NullPointerException์ด ๋ฐ์ํ๋ค๋ฉด, @ExceptionHandler(NullPointerException.class)๊ฐ ์ ์ฉ๋ ๋ฉ์๋๊ฐ ํธ์ถ๋ ๊ฒ์ด๋ค.
@RestController public class TestController { ... ... @ExceptionHandler(NullPointerException.class) public String nullpointers(Exception e) { System.err.println(e.getClass()); return "error message"; } }
ํ์ง๋ง, @ExceptionHandler๋ฅผ ๋ฑ๋กํ Controller์๋ง ์ ์ฉ๋๋ค. ๋ค๋ฅธ Controller์์ NullPointerException์ด ๋ฐ์ํ๋๋ผ๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ค๋ ๋จ์ ์ด ์๋ค.
๐ @ControllerAdvice
@ExceptionHandler๊ฐ ํ๋์ ํด๋์ค์ ๋ํ ๊ฒ์ด๋ผ๋ฉด, @ControllerAdvice๋ ๋ชจ๋ @Controller ์ฆ, ์ ์ญ์์ ๋ฐ์ํ ์ ์๋ ์์ธ๋ฅผ ์ก์ ์ฒ๋ฆฌํด์ฃผ๋ annotation์ด๋ค.
์ด ์ค์์๋, @RestControllerAdvice์ @ControllerAdvice 2๊ฐ์ง ์ ํ์ด ์๋ค.
@RestControllerAdvice๋ @ResponseBody๊ฐ ์ถ๊ฐ ๋์ด ์๋ค๋ ์ฐจ์ด๊ฐ ์๋ค.
๋ณธ ํฌ์คํ ์์๋ @ControllerAdvice๋ฅผ ์ฐ๋, ํ์ํ ๋ถ๋ถ์๋ง @ResponseBody๋ฅผ ๋ถ์ฌ ์ค๋ค.์ด ๊ธ์์ ๋ณด์ฌ์ฃผ๊ณ ์ ํ๋๊ฒ๋ค์ ๋๋ต ์ด๋ ๋ค.
๋ฐ์ํ๋ ์๋ฌ๋ค์ ์ก์์ HttpCode 500 ๊ฐ์ ์๋ฌ๋ฅผ ์๋ตํ์ง ์๊ณ 200์ ์๋ตํ๋,
์ ์ธํ ์๋ฌ Code์ Message๋ฅผ ๋ด๋ ค์ฃผ๊ณ ํ๋ฉด์์ ๊ทธ์ ๋ง์ถฐ์ ๋์์ ํ ์ ์๋ค.๊ณตํต์ ์ผ๋ก ๋ฐ์ํ๋ Error๋ค์ ๋ํด ์ ์ํด๋ Error Code์ Message๋ฅผ ์๋ตํ๋ค.
ServiceException์ ์ ์ํด์ ๊ฐ ์๋น์ค์์ ์ํ๋ Error๋ฅผ ๋์ ธ์ค ์ ์๋ค.
๐ ์ค์ต ์ ์ ํ์ํ Class๋ค์ ์์ฑ ํด๋ณธ๋ค.
๐ ResultCode.class
public enum ResultCode { SUCCESS(200, ResultMessage.SUCCESS), UNAUTHORIZED(401, ResultMessage.UNAUTHORIZED), NO_AUTH(403, ResultMessage.NO_AUTH), INTERNAL_ERROR(500, ResultMessage.INTERNAL_ERROR), ACCESS_NO_AUTH(1_000, ResultMessage.ACCESS_NO_AUTH), ACCESS_TOKEN_EXPIRED(1_001, ResultMessage.ACCESS_TOKEN_EXPIRED), REFRESH_TOKEN_EXPIRED(1_002, ResultMessage.REFRESH_TOKEN_EXPIRED), VALID_NOT_PHONE_NUM(1_007, ResultMessage.VALID_NOT_PHONE_NUM), VALID_NOT_PASSWORD(1_008, ResultMessage.VALID_NOT_PASSWORD), MEMBER_NOT_EXIST(1_012, ResultMessage.MEMBER_NOT_EXIST), LOGIN_REQUIRED(1_019, ResultMessage.LOGIN_REQUIRED), PARAM_NOT_VALID(2_000, ResultMessage.PARAM_NOT_VALID), ; private final int resultCode; private final String resultMessage; ResultCode(int resultCode, String resultMessage) { this.resultCode = resultCode; this.resultMessage = resultMessage; } public int getResultCode() { return resultCode; } public String getResultMessage() { return resultMessage; } public interface ResultMessage { String SUCCESS = "์๋ฃ ๋์์ต๋๋ค."; String UNAUTHORIZED = "์ธ์ฆ์ ์คํจํ์์ต๋๋ค."; String NO_AUTH = "์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค."; String ACCESS_NO_AUTH = "์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค."; String ACCESS_TOKEN_EXPIRED = "Access Token์ด ๋ง๋ฃ ๋์์ต๋๋ค."; String REFRESH_TOKEN_EXPIRED = "Refresh Token์ด ๋ง๋ฃ ๋์์ต๋๋ค."; String VALID_NOT_PHONE_NUM = "๊ฐ์ ๋์ง ์์ ํธ๋ํฐ ๋ฒํธ ์ ๋๋ค."; String VALID_NOT_PASSWORD = "์๋ชป๋ ๋น๋ฐ๋ฒํธ ์ ๋๋ค."; String MEMBER_NOT_EXIST = "์กด์ฌํ์ง ์๋ ์ฌ์ฉ์ ์ ๋๋ค."; String LOGIN_REQUIRED = "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค."; String PARAM_NOT_VALID = "ํ๋ผ๋ฏธํฐ ์ค๋ฅ์ ๋๋ค."; String INTERNAL_ERROR = "์์คํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์."; } }
- Error Code์ Message๋ฅผ ์ ์ํ Common Enum Class๋ฅผ ์์ฑํ๋ค.
- ํ์ํ Code์ Message๋ค์ ์ถ๊ฐ ํ์ฌ ์ฌ์ฉํ๋ฉด ๋๋ค.
๐ BaseResDto.class
- Error Code์ Message๋ฅผ ๋ด์ Dto Class๋ฅผ ์์ฑํ๋ค.
- ์ด Class๋ Error๊ฐ ๋ฐ์์ Returnํ Common Response Class์ด๋ค.
@Getter @Setter public class BaseResDto { private int resultCode = ResultCode.SUCCESS.getResultCode(); private String resultMessage = ResultCode.SUCCESS.getResultMessage(); }
๐ ServiceException.class
@RequiredArgsConstructor public class ServiceException extends Exception { private final int resultCode; private final String resultMessage; public ServiceException(@NonNull ResultCode resultCodeEnum) { this.resultCode = resultCodeEnum.getResultCode(); this.resultMessage = resultCodeEnum.getResultMessage(); } public ServiceException(@NonNull ResultCode resultCodeEnum, @NonNull Map<String, Object> params) { this.resultCode = resultCodeEnum.getResultCode(); String messageTemplate = resultCodeEnum.getResultMessage(); for (Map.Entry<String, Object> entry : params.entrySet()) { messageTemplate = messageTemplate.replaceAll(String.format("\\$\\{%s\\}", entry.getKey()), String.valueOf(entry.getValue())); } this.resultMessage = messageTemplate; } public int getResultCode() { return resultCode; } public String getResultMessage() { return resultMessage; } }
- Exception์ ์์ ๋ฐ์ ๊ณตํต Exception Class์ด๋ค.
- ์ํ๋ ์๋น์ค๋จ ๋ก์ง์์ ์ด Exception์ throw ํด์ฃผ๋ฉด ๋๋ค.
- ๋ ๋ฒ์งธ ์์ฑ์๋ ResultCode Enum Class์์ ๋ฉ์ธ์ง๋ฅผ ${field}์ ๊ฐ์ด ์ ์ํ์ฌ
์ธ ์ ์๋ค.
Ex) ${field} ํ๋๋ ํ์ ๊ฐ์ ๋๋ค.
๐ GlobalExceptionHandler.class
๐ก Points
@ControllerAdvice, @RestController๋ฅผ ์ ์ธํ๊ณ , ExceptionHandler๋ฅผ ์ ์ํ Class
@ExceptionHandler: Catchํ Exception์ ์ ์ธ
ExceptionHandler์ ์ ์ธํ Class๋ฅผ Parameter๋ก ๋ฐ๋๋ค.
์ด Handler Method๋ค์ Return Class๋ ์์์ ์ ์ํ BaseResDto๋ฅผ ์ฌ์ฉ
-> Common Response Class๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํจ๐ก ๋์ ์์
ControllerAdvice๊ฐ ExceptionHandler์ ์ ์ธํ Exception ๋ฐ์์ ๊ฐ Method๋ฅผ ํธ์ถํ๋ค.
@ResponseStatus์ HttpStatus.OK๋ฅผ ์ ์ธํด์ HTTP Code 200์ ์๋ตํ๊ฒ ํ๊ณ ,
BaseResDto์ ์ ๋ฌ ๋ฐ์ ResultCode, ResultMessage๋ฅผ Setํด์ Returnํ๋ ํํ์ด๋ค. ์ด๋ ๊ฒ ํ๋ฉด ํ๋ฉด์์๋ 500 Error ๊ฐ ์๋๋ผ 200 Code๋ฅผ ์๋ต ๋ฐ๊ณ , ๊ฐ ResultCode์ ๋ฐ๋ผ ResultMessage๋ฅผ ์ถ๋ ฅํด์ฃผ๋ ๋ฑ, ์ฌ์ฉ์ ์ ์ ์ฒ๋ฆฌ๋ฅผ ํ ์ ์๋ค.@Slf4j @ControllerAdvice @RestController public class GlobalExceptionHandler { private static final String field = "${field}"; 1๏ธโฃ @ExceptionHandler(com.package.common.exception.ServiceException.class) @ResponseStatus(HttpStatus.OK) public BaseResDto exception(com.package.common.exception.ServiceException e) { BaseResDto baseResDto = new BaseResDto(); baseResDto.setResultCode(e.getResultCode()); baseResDto.setResultMessage(e.getResultMessage()); log.error("[{}]ServiceException: code[{}], message[{}]", ContextUtil.reqInfo.get().getUuid(), baseResDto.getResultCode(), baseResDto.getResultMessage()); return baseResDto; } 2๏ธโฃ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.OK) public BaseResDto exception(BindException e) { BaseResDto baseResDto = new BaseResDto(); FieldError fieldError = e.getBindingResult().getFieldError(); if (fieldError == null) { baseResDto.setResultCode(ResultCode.INTERNAL_ERROR.getResultCode()); baseResDto.setResultMessage(ResultCode.INTERNAL_ERROR.getResultMessage()); log.error("[{}]Internal Exception: {}", ContextUtil.reqInfo.get().getUuid(), ExceptionUtils.getStackTrace(e)); return baseResDto; } String code = fieldError.getCode(); if ("NotNull".equals(code) || "NotEmpty".equals(code) || "NotBlank".equals(code)) { baseResDto.setResultCode(ResultCode.VALID_NOT_NULL.getResultCode()); baseResDto.setResultMessage(ResultCode.VALID_NOT_NULL.getResultMessage().replace(field, fieldError.getField())); } else if ("Pattern".equals(code)) { baseResDto.setResultCode(ResultCode.VALID_NOT_REGEXP.getResultCode()); baseResDto.setResultMessage(ResultCode.VALID_NOT_REGEXP.getResultMessage().replace(field, fieldError.getField())); } else if ("MaxByte".equals(code)) { baseResDto.setResultCode(ResultCode.PARAM_NOT_VALID.getResultCode()); baseResDto.setResultMessage(String.format("%s ๊ฐ์ด %dbyte ๋ณด๋ค ํฝ๋๋ค.", fieldError.getRejectedValue(), fieldError.getArguments()[1])); } else { baseResDto.setResultCode(ResultCode.PARAM_NOT_VALID.getResultCode()); baseResDto.setResultMessage(ResultCode.PARAM_NOT_VALID.getResultMessage()); } log.error("[{}]ValidationException: message[{}]", ContextUtil.reqInfo.get().getUuid(), e.getMessage()); return baseResDto; } 3๏ธโฃ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.OK) public BaseResDto exception(MissingServletRequestParameterException e) { BaseResDto baseResDto = new BaseResDto(); baseResDto.setResultCode(ResultCode.VALID_NOT_NULL.getResultCode()); baseResDto.setResultMessage(ResultCode.VALID_NOT_NULL.getResultMessage().replace(field, e.getParameterName())); log.error("[{}]ValidationException: message[{}]", ContextUtil.reqInfo.get().getUuid(), ExceptionUtils.getStackTrace(e)); return baseResDto; } 4๏ธโฃ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.OK) public BaseResDto exception(Exception e) { BaseResDto baseResDto = new BaseResDto(); baseResDto.setResultCode(ResultCode.INTERNAL_ERROR.getResultCode()); baseResDto.setResultMessage(ResultCode.INTERNAL_ERROR.getResultMessage()); log.error("[{}]Internal Exception: {}", ContextUtil.reqInfo.get().getUuid(), ExceptionUtils.getStackTrace(e)); return baseResDto; } }
- 1๏ธโฃ: ์์์ ์์ฑํ ServiceException์ ํตํด ์ ๋ฌ ๋ฐ์ ResultCode, ResultMessage๋ฅผ BaseResDto์ set ํด์ค ๋ค Return. (ํ์์ ์ฝ๋์ ๊ฐ์ด log ์ถ๋ ฅ)
- 2๏ธโฃ: BindException์ ๋ณดํต Parameter์ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌ ๋ฐ๊ฑฐ๋, @NotNull @NotEmpty ๊ฐ์ ์กฐ๊ฑด์ ๊ฑธ๋ ธ์๋ ๋ฐ์ํ๋ค. BindingResult ๋ฉ์๋๋ฅผ ํตํด FieldError๋ฅผ ๊ฐ์ ธ์จ ๋ค ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ํตํด ์ํ๋ ResultCode์ ResultMessage๋ฅผ set ํด์ค๋ค.
- 3๏ธโฃ, 4๏ธโฃ๋ ๋์ผํ ํํ์ด๋ค. ์ด๋ ๊ฒ ์ํ๋ Exception์ ์๋ตํ๊ณ ์ถ์ ResultCode์ Message๋ฅผ Return ํด์ฃผ๋ฉด ๋๋ค.
๐ ์์
@Getter public class UserResDto extends BaseResDto { private String userId; private String name; public UserResDto(User user) { this.userId = user.getUserId(); this.name = user.getName(); } }
@GetMapping("user-info/{userId}") public BaseResDto getUserInfo(@PathVariable("userId") String userId) { return userService.getUserInfo(userId); }
public BaseResDto getUserInfo(String userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new ServiceException(ResultCode.USER_NOT_EXIST)); return new UserResDto(user); }
- BaseResDto๋ฅผ ์์ ๋ฐ์ User์ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ Dto Class๋ฅผ ์์ฑ ํ๋ค.
- ํด๋น ์ ์ ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ API๋ฅผ ํธ์ถ ํ์ ๋, UserService์ getUserInfoํจ์๋ฅผ ํธ์ถํ๋ค. JPA๋ก User Entity๋ฅผ ์กฐํํ๋ ๋์์ orElseThrow๋ฅผ ํตํด ํด๋น ์ ์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ์ง ์์๋ ServiceException์ ๋ฏธ๋ฆฌ ์ ์ ํด๋์ ResultCode๋ฅผ ๋ด์์ throwํ๋ค.
- ๊ทธ๋ฌ๋ฉด ControllerAdvice Annotation์ ์ ์ธํด๋์ GlobalExceptionHandler๊ฐ
@ExceptionHandler(ServiceException.class)๊ฐ ์ ์ธ ๋์ด ์๋ Method๋ฅผ ํธ์ถํ๋ค. - ์ ๋ฌ ๋ฐ์ ResultCode.USER_NOT_EXIST์ ResultCode, ResultMessage๋ฅผ BaseResDto์ setํด์ Returnํ๋ค.
๐ ์๋น์ค ํจ์์ Return Type์ด BaseResDto์ธ ์ด์ ๋ UserResDto๊ฐ ํด๋น ํด๋์ค๋ฅผ ์์(Is a) ๋ฐ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฅํ ๊ฒ์ด๋ค. ์ด๋ฐ์์ผ๋ก Response Dto์ BaseResDto๋ฅผ ์์๋ฐ์์ ๊ฐ API๋ง๋ค Return Class๋ฅผ BaseResDto๋ก ์ ์ธ ํ๋ฉด ๋๋ค.
๋ฐ์ํ'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring] Spring์์ CORS ์ฒ๋ฆฌ(์ค์ )ํ๋ ๋ฐฉ๋ฒ (0) 2022.10.27 [Spring] Spring์์ Scheduler ์ฒ๋ฆฌํ๊ธฐ (0) 2022.10.26 Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (3) (0) 2022.10.23 Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (2) (0) 2022.10.23 Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (1) (0) 2022.10.23