@ControllerAdvice 와 @ExceptionHandler를 이용한 예외처리
개인적으로 평소에 일이나 개인 공부를 할때 가장 신경쓰는 것은 예외처리이다.
개발자와 사용자의 시각차이는 분명 존재하고 항상 개발자가 의도한대로 사용자가 프로그램을 사용할 수는 없다.
난 개인적으로 완벽한 개발을 할 수 없는 사람이기때문에 여러가지 예외상황이 일어날 것은 항상 염두해두며 작업을
진행한다. 그래서 예외처리를 굉장히 중요하게 생각한다. 어떠한 상황이 일어날지 100퍼센트 예측할 수 없기때문에..
평소와 다를 것 없이 일하고 있는 와중에 내가 작성한 코드를 보니 try catch 가 너무나 남발되고 있다는 느낌을 받았다.
그래서 이번에는 @ControllerAdvice 어노테이션과 @ExceptionHandler 어노테이션을 이용해 컨트롤러단에서
발생되는 예외상황을 캐치하여 공통적으로 처리하고 코드를 조금 더 깔끔하고 간결하게 작성할 수 있는 부분에 대해서
글을 작성해보고자 한다.
예제는 지난번에 글을 작성하며 작성했던 사용자 인증서비스 소스를 가지고 작성해보았다.
<로그인 컨트롤러>
@RequestMapping(value = {"/signin"}, method = {RequestMethod.POST},
params = {"id", "password"})
public ResponseEntity<String> apiUserSignin(
HttpServletRequest request,
HttpServletResponse response) throws Exception {
Map<String, Object> resultMap = new HashMap<>();
try {
Map<String, Object> dataMap = validateParams(request);
if(IsEmpty.check(dataMap)) {
resultMap.put("result", false);
return JSONUtil.returnJSON(response, resultMap);
}
resultMap = apiSignService.loginUserProcessService(dataMap);
} catch (Exception e) {
e.printStackTrace();
resultMap.put("result", false);
return JSONUtil.returnJSON(response, resultMap, HttpStatus.INTERNAL_SERVER_ERROR);
}
return JSONUtil.returnJSON(response, resultMap);
}
<로그인 서비스>
@Override
public Map<String, Object> loginUserProcessService(Map<String, Object> dataMap) {
Map<String, Object> resultMap = new HashMap<>();
try {
Map<String, Object> resultData = apiSignDao.selectUserInfoById(dataMap);
if(IsEmpty.check(resultData)) {
resultMap.put("result", false);
resultMap.put("msg", "NO_EXIST_DATA");
return resultMap;
}
if(!passwordEncoder.matches(dataMap.get("password").toString(),
resultData.get("member_password").toString())) {
resultMap.put("result", false);
resultMap.put("msg", "PASSWORD_DO_NOT_MATCH");
return resultMap;
}
String token = jwtTokenProvider.createToken(resultData);
resultData.remove("member_password");
resultData.put("x-access-token", token);
resultMap.put("data", resultData);
} catch (Exception e) {
e.printStackTrace();
resultMap.put("result", false);
resultMap.put("msg", "INTERNAL_SERVER_ERROR");
}
return resultMap;
}
위에 보이는 거와 같이 try 부분에서 요청 로직을 수행하고 문제가 발생되면 catch에서 해당 예외처리에 대한
메시지와 결과 상태를 담아 리턴하는 방식을 사용했다. 현재로서는 별 문제는 없다고 생각하지만 이 코드가 길면
길어질수록 try catch 블록은 기하급수적으로 늘어날 것이다. 그렇다면 당연히 코드도 보기 힘들어지겠지....
그리고 여러가지 글을 찾으면서 느낀 부분은 예외상황이 발생했을 때 굳이 map객체에 담아 데이터를 리턴할 필요도
없다는 것이었다. 그래서 공통적으로 처리하는 부분을 간소하게나마 만들어보았다.
<ExceptionAdvice>
@RequiredArgsConstructor
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({Exception.class, UserNotFoundException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
protected ResponseEntity<String> defaultException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
@ExceptionHandler({AuthenticationEntryPointException.class, AccessDeniedException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<String> unauthorizedException(Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
@ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class,
MissingServletRequestParameterException.class, UnsatisfiedServletRequestParameterException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> badRequestException(Exception e) throws Exception {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(ForbiddenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<String> forbiddenException(ForbiddenException e) throws Exception {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
}
@ExceptionHandler(Code700Exception.class)
public ResponseEntity<String> Error700Exception(Code700Exception e) throws Exception {
e.printStackTrace();
return ResponseEntity.status(700).body(e.getMessage());
}
}
자 여기서 눈여겨봐야 할 것은 @ControllerAdvice 어노테이션과 @ExceptionHandler 어노테이션이다.
@ControllerAdvice 어노테이션을 선언하게 되면 그 후부터는 컨트롤러는 이 어노테이션에게 감시를 받게된다.
그렇다면 어떤 부분을 감시받느냐?? 바로 예외상황에 대한 부분을 감시받게 된다.
감시하는 도중에 컨트롤러에서 예외 상항이 발생하면 @ExceptionHandler가 선언된 메서드를 확인하고,
@ExceptionHandler 인자로 들어오는 예외가 발생하게 되면 해당 메서드가 예외상황을 처리하게 된다.
이제 컨트롤러에서는 try catch를 사용하지 않아도 catch에 대한 처리가 가능해진 것이다.
<로그인 컨트롤러>
@RequestMapping(value = {"/signin"}, method= {RequestMethod.POST},
params = {"id", "password"})
public ResponseEntity<String> apiUserSignin(
ModelMap model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
Map<String, Object> dataMap = validateParams(request);
Map<String, Object> resultMap = cmsSignSerivce.loginUserProcessService(dataMap);
return JSONUtil.returnJSON(response, resultMap);
}
<로그인 서비스>
@Override
public Map<String, Object> loginUserProcessService(Map<String, Object> dataMap) {
Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> resultData = apiSignDao.selectUserInfoById(dataMap);
if(IsEmpty.check(resultData)) {
throw new Code700Exception("There is no Result Data");
}
if(!passwordEncoder.matches(dataMap.get("password").toString(),
resultData.get("member_password").toString())) {
throw new ForbiddenException("Passwords do not match");
}
String token = jwtTokenProvider.createToken(resultData);
resultData.remove("admin_password");
resultData.put("x-access-token", token);
resultMap.put("data", resultData);
return resultMap;
}
이런 식으로 컨트롤러는 그저 전달과 응답에 대한 부분만 수행하면 되고, 서비스는 각 예외상황에 맞는 예외를
발생시키며 비즈니스 로직을 수행하면 된다. 그럼 예외처리는 모두 어드바이스 내에서 처리하게 된다.
솔직히 처음에는 큰 차이를 못 느꼈다. 코드가 길면 길어질수록 찾기 힘들어지고 블록은 더더욱 늘어나게 될 상황이
줄어드는 것을 직접 체감하다 보니 앞으로 스프링 프로젝트를 새로 시작하게 되더라도 예외처리는 기본적으로
공통 처리로 세팅해놓고 작업하게 될 것 같다.