문: 이미 한~참 진행된 스프링 개인과제(총 3번의 과제가 합쳐진)에 전역 예외처리(Global ExceptionHandling
)를 해야했다.
시: Error Code Enum
부터 작성하려 했으나, 제한된 시간으로 인해 일단 작성된 코드를 기반으로 Global ExceptionHandling
을 공부하고 적용하는 것을 목표로 진행하였다.
해: @RestControllerAdvice
를 바탕으로 전역예외처리를 성공?! 한 줄 알았으나 .. 한 가지 문제가 더 있었다~!
알: Global ExceptionHandling
를 위해 필요한 다양한 어노테이션들을 알 수 있었고, 한 걸음 더 나아가 throw
하는 법에 대해서도 더 공부할 수 있었다.
스프링 숙련 과제 LV2에서 예외처리에 대한 명세를 만났다.
- 명확히 명시된 것은 아니었지만, 이전부터 궁금해서 찾아봐두었던
전역예외처리
를 도전해보기로 결심했다.
[스프링부트] @ExceptionHandler를 통한 예외처리
enum ErrorCode
을 작성하여 한 곳에서 모든 에러코드들을 관리하고 싶었다.RuntimeException
, IllegalArgumentException
을 상속받은 예외들이 주루룩 있는 상태)// 내가 선언한 Exception 예시
public class CommentNotExistException extends IllegalArgumentException{
public CommentNotExistException(){super("댓글이 존재하지 않습니다");}
}
Global ExceptionHandling
을 해보기로 결정하였다.@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ CustomException.class })
protected ResponseEntity handleCustomException(CustomException ex) {
return new ResponseEntity(new ErrorDto(ex.getErrorCode().getStatus(), ex.getErrorCode().getMessage()), HttpStatus.valueOf(ex.getErrorCode().getStatus()));
}
@ExceptionHandler({ CustomException.class })
- 내가 선언한
CommentNotExistException
도 이런식으로 잡을 수 있겠다는 점.java
가 아니라.class
로 쓴다는 점에서 예외처리가 진행되는 시점에 대한 생각을 할 수 있었고,return
쪽을 보면서 글 윗부분에 작성했던enum ErrorCode
를 이렇게 값을 뽑아서 리턴해주는 식으로 사용하면서 하나의 핸들러로 모든 CustomException을 커버할 수 있구나~ 라는 점을 배울 수 있었다.
@ExceptionHandler({UserNotExistException.class, MissmatchPasswordException.class})
protected ResponseEntity<String> handleLoginException(UserNotExistException e1, MissmatchPasswordException e2) {
if (e1.getMessage().isEmpty()){
return new ResponseEntity<>(e2.getMessage(), HttpStatus.UNAUTHORIZED);
}
return new ResponseEntity<>(e1.getMessage(), HttpStatus.UNAUTHORIZED);
}
당연히! 원하는대로 핸들링이 되지 않았다.
- 하나의 Exception만 잡는 핸들러는 잘 작동했으나 위 핸들러는 작동하지 않았다.
parameter
에 e1, e2가 동시에 들어올 수가 없으니 작동하지 않는게 당연했다.
(생각나는 대로 한다고, 기본 중의 기본을 생각하지 못하다니...)- 다시 차분하게 코드를 수정했다.
@ExceptionHandler({UserNotExistException.class, MissmatchPasswordException.class})
protected ResponseEntity<String> handleLoginException(IllegalArgumentException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
잘 잡힌다!
@ExceptionHandler
옆에 쓰는 '내가 잡길 원하는 예외'들은 자세하게 써야한다.parameter
의 Type은 모든 예외들을 포괄하는 RuntimeException으로 설정해두는 것이 좋다.- HTTP Status Code는 무작정 2XX, 4XX을 던지는 것보다 의미에 맞는 Code를 보내주는 연습을 하자.
1. 전역 예외처리(Global ExceptionHandling)하는 법을 알게되었다.
- enum ErrCode를 작성하고, 이를 활용해서
전역예외처리
하는 방법과,- 기존에 작성된 코드들(많은 예외클래스 들이 존재하는)에서
전역예외처리
하는 방법을 모두 배울 수 있었다.
2. HTTP Status Code에 대해 더욱 생각하고 return하게 되었다.
3. 던지기(Throw)도 잘 던져야하고, 받기(Handling)도 잘 받아야 된다는 점을 배웠다.
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
JWT 강의에서 가르쳐준 방식에서는
validateToken()
으로 검증하고,getUserInfoFromToken()
으로 값을 꺼내오는 방식으로 진행되었었다.- 사실상 같은 코드를 2번 반복하는 것이기도 하고, 친구를 통해 JWT verify(디코딩하는)비용이 비싸다는 얘기도 들은터라 위 매커니즘을 축약하고 싶었다.
- 그래서 아래와 같이 유저정보를 꺼내는 과정에서 예외가 발생할 경우
RuntimeException
을 상속한AuthException
을 throw하도록 설계하였다.
public Claims getUserInfoFromToken(String token) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
} catch (Exception e) {
log.info("AuthException");
throw new AuthException();
}
}
SpringDocs- Exceptions
SpringDocs- Controller-Advice
테코블-전역예외처리
ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까.
exceptionHandling
@ExceptionHandler를 통한 예외처리