앞서 설명한 방식에 한계를 느끼고 예외들을 클래스가 아닌 yml 파일로 작성하여 사용하는것이 좀더 간편하고 유연하지 않을까?
라는 생각을 하게 되었고 이 글에서 해당 내용을 작성해보려한다.
예외 처리 라이브러리 로직
먼저, 앱에서 사용할 커스텀 예외들을 통칭하는 BizException 클래스를 생성한다.
@Getter
public class BizException extends RuntimeException {
private final String key;
public BizException(String key) {
super(key);
this.key = key;
}
}
해당 클래스는, yml에서 설정한 예외들을 선택할 key 값을 가지고 있다.
@Getter
@ToString
public class ErrorConfig {
private Map<String, ErrorInfo> errors = new HashMap<>();
public static ErrorConfig build() throws Exception {
return new ErrorConfig().setResource("/error/exception.yml");
}
public ErrorConfig setResource(String path) throws Exception {
Resource resource = new ClassPathResource(path);
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
this.errors = mapper.readValue(resource.getInputStream(), mapper.getTypeFactory().constructMapType(HashMap.class, String.class, ErrorInfo.class));
return this;
}
}
그리고 ErrorConfig 클래스는, resoureces 폴더아래에 특정 경로에서 exception.yml 파일을 읽어와서 Map 형태로 저장하는 Config 클래스이다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ErrorInfo {
private int status;
private String message;
}
그리고 해당 클래스는, 클라이언트의 응답을 반환해줄 형식이다. 상태 코드와 에러 메세지를 반환한다.
마지막으로, 핸들러 설정이다.
@Slf4j
@AllArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RestExceptionHandler implements HandlerExceptionResolver {
private ErrorConfig errorConfig;
private final ObjectMapper objectMapper = new ObjectMapper();
private final static List<Class<? extends Exception>> BUSINESS_EXCEPTION = List.of(BizException.class);
private final static List<Class<? extends Exception>> BAD_REQUEST = List.of(MethodArgumentNotValidException.class, HttpMessageNotReadableException.class, TypeMismatchException.class, MissingServletRequestParameterException.class, BindException.class);
private final static List<Class<? extends Exception>> UNAUTHORIZED = List.of(AuthenticationException.class, AccessDeniedException.class);
private final static List<Class<? extends Exception>> NOT_FOUND = List.of(NoHandlerFoundException.class, NoResourceFoundException.class);
private final static List<Class<? extends Exception>> METHOD_NOT_ALLOWED = List.of(HttpRequestMethodNotSupportedException.class);
private final static List<Class<? extends Exception>> UNSUPPORTED_MEDIA_TYPE = List.of(HttpMediaTypeNotSupportedException.class);
private final static List<Class<? extends Exception>> INTERNAL_SERVER_ERROR = List.of(Exception.class);
public static RestExceptionHandler setErrorConfig(ErrorConfig errorConfig) {
return new RestExceptionHandler(errorConfig);
}
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ErrorInfo errorInfo = null;
if (isRequestMatch(ex, BUSINESS_EXCEPTION)) errorInfo = setBizErrorInfo((BizException) ex);
else if (isRequestMatch(ex, BAD_REQUEST)) errorInfo = ErrorInfo.builder().status(400).message("잘못된 요청입니다. 요청 파라미터를 확인해주세요.").build();
else if (isRequestMatch(ex, UNAUTHORIZED)) errorInfo = ErrorInfo.builder().status(401).message("접근 권한이 없습니다. 관리자에게 문의하세요.").build();
else if (isRequestMatch(ex, NOT_FOUND)) errorInfo = ErrorInfo.builder().status(404).message("요청하신 리소스를 찾을 수 없습니다. URL을 확인해주세요.").build();
else if (isRequestMatch(ex, METHOD_NOT_ALLOWED)) errorInfo = ErrorInfo.builder().status(405).message("허용되지 않은 메소드입니다. 요청 방식을 확인해주세요.").build();
else if (isRequestMatch(ex, UNSUPPORTED_MEDIA_TYPE)) errorInfo = ErrorInfo.builder().status(415).message("지원하지 않는 미디어 타입입니다. 요청 데이터를 확인해주세요.").build();
else if (isRequestMatch(ex, INTERNAL_SERVER_ERROR)) errorInfo = ErrorInfo.builder().status(500).message("서버 내부 오류가 발생했습니다. 관리자에게 문의하세요.").build();
setErrorInfoToResponse(response, ex, errorInfo);
return new ModelAndView();
}
private boolean isRequestMatch(Exception ex, List<Class<? extends Exception>> exceptionList) {
return exceptionList.stream().anyMatch(clazz -> clazz.isInstance(ex));
}
private ErrorInfo setBizErrorInfo(BizException ex) {
ErrorInfo errorInfo = null;
errorInfo = errorConfig.getErrors().get(ex.getKey());
if (errorInfo == null) errorInfo = ErrorInfo.builder().status(500).message("알 수 없는 에러입니다.").build();
return errorInfo;
}
private void setErrorInfoToResponse(HttpServletResponse response, Exception ex, ErrorInfo errorInfo) {
log.error(ex.getClass().getSimpleName() + " : " + errorInfo.getMessage());
ex.printStackTrace();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(errorInfo.getStatus());
response.setCharacterEncoding("UTF-8");
try {
response.getWriter().write(objectMapper.writeValueAsString(errorInfo));
} catch (IOException e) {
e.printStackTrace();
}
}
}
기본적으로 발생할 수 있는 7개의 예외들에 대한 에러 인포들은 코드에서 직접 생성하였고,
그 이외의 예외들은 BizException의 key값을 받아와 errorInfo 에서 찾아서 적절한 응답을 생성하고, 만약 key가 존재하지 않는 커스텀 예외는 500에러를 반환하도록 설정한 것이다.
예외 처리 라이브러리 사용
이제 저렇게 생성한 라이브러리를 사용하는 방식에 대한 설명이다.
해당 라이브러리 임포트 후, 아래와 같이 configuration에 등록한다.
@Configuration
public class WebConfig {
@Bean
public RestExceptionHandler setRestExceptionHandler() throws Exception {
ErrorConfig errorConfig = ErrorConfig.build();
return RestExceptionHandler.setErrorConfig(errorConfig);
}
}
그 후, resources/error 폴더 아래에 exception.yml 파일을 생성하여 다음과 같이 커스텀 에러들을 추가한다.
### JWT ###
token_expired:
status: 403
message: 토큰이 만료 되었습니다.
token_not_valid:
status: 403
message: 토큰이 유효하지 않습니다.
### USER ###
user_not_found:
status: 404
message: 해당 사용자를 찾을 수 없습니다.
user_already_exist:
status: 409
message: 이미 가입된 유저입니다.
이렇게 설정 후, 예외 생성이 필요한 경우 아래와 같이 생성한다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(!PERMIT_URL.contains(request.getServletPath())) {
String accessToken = jwtTokenProvider.resolveToken(request);
if(accessToken == null || accessToken.isEmpty()) throw new BizException("token_not_valid");
setAuthentication(accessToken, request);
}
filterChain.doFilter(request, response);
}
해당 부분에서 처럼 BizException을 던졌고, 해당 Exception은 핸들러에 잡혀 token_not_valid 라는 응답으로 클라이언트에게 반환되게 된다.
{
"status": 403,
"message": "토큰이 유효하지 않습니다."
}
결론
설명한 방법처럼 하면 먼저 아래와 같은 장점들이 있다.
1. yml 파일에 예외를 관리하기 떄문에, 각 예외마다 따로 클래스를 생성하지 않아도 된다.
2. 외부 파일을 참조하도록 설정하여, 컴파일 후에도 에러 내용을 변경할 수 있다.
3. 라이브러리화 하여 단 3줄과 yml 파일만 생성하면 사용자 정의 예외를 생성하고 응답을 반환할 수 있다.
이러한 장점들로, 현재는 모든 어플리케이션은 위와같은 방법으로 사용자 예외를 처리하고 있다.
만약, 사용할 의향이 있으시면 개인 넥서스에 등록해두었으니 아래와 같이 build.gradle 을 수정하여 사용하면 된다.
repositories {
mavenCentral()
maven {
url 'https://woonexus.site/repository/woo-maven-repo/'
}
}
dependencies {
...
implementation 'com.woo:rest-exception:1.0.0'
...
}
'Web > Spring Boot 예외 처리' 카테고리의 다른 글
[Spring Boot] 예외 처리 라이브러리 만들기 (2) (0) | 2024.08.13 |
---|---|
[Spring Boot] 예외 처리 라이브러리 만들기 (1) (0) | 2024.08.13 |