개인적으로 평소에 일이나 개인 공부를 할때 가장 중요하게 느끼는 것 중 하나는 바로 예외처리이다. 개발자와 사용자의 시각차이는 분명 존재하고 항상 개발자가 의도한대로 사용자가 프로그램을 사용할 수는 없다. 난 개인적으로 완벽한 개발을 할 수 없는 사람이기때문에 여러가지 예외상황이 일어날 것은 항상 염두해두며 작업을 진행한다.
평소와 다를 것 없이 일하고 있는 와중에 내가 작성한 코드를 보니 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객체에 담아 데이터를 리턴할 필요도 없다는 것이었다. 그래서 공통적으로 처리하는 부분을 간소하게나마 만들어보았다.
@RequiredArgsConstructor
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({Exception.class, UserNotFoundException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public 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 어노테이션을 선언하게 되면 그 후부터는 @Controller 어노테이션이 선언된 클래스는 어노테이션에게 감시를 받게된다.
그렇다면 어떤 부분을 감시받느냐?? 바로 예외상황에 대한 부분을 감시받게 된다.
감시하는 도중에 컨트롤러에서 예외 상항이 발생하면 @ExceptionHandler가 선언된 메서드를 확인하고, @ExceptionHandler 인자로 들어오는 예외가 발생하게 되면 해당 메서드가 예외상황을 처리하게 된다.
이제 컨트롤러에서는 try catch를 사용하지 않아도 catch에 대한 처리가 가능해진 것이다.
@RequestMapping(value = {"/signin"}, method= {RequestMethod.POST}, params = {"id", "password"})
public ResponseEntity<String>apiUserSignin (
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;
}
이런 식으로 컨트롤러는 그저 전달과 응답에 대한 부분만 수행하면 되고, 서비스는 각 예외상황에 맞는 예외를 발생시키며 비즈니스 로직을 수행하면 된다. 그럼 예외처리는 모두 어드바이스 내에서 처리하게 된다.
솔직히 처음에는 큰 차이를 못 느꼈다. 코드가 길면 길어질수록 찾기 힘들어지고 블록은 더더욱 늘어나게 될 상황이 줄어드는 것을 직접 체감하다 보니 앞으로 스프링 프로젝트를 새로 시작하게 되더라도 예외처리는 기본적으로 공통 처리로 세팅해놓고 작업하게 될 것 같다.