@ControllerAdvice 와@ExceptionHandler

shinhyocheol·2021년 7월 5일
0

개인적으로 평소에 일이나 개인 공부를 할때 가장 중요하게 느끼는 것 중 하나는 바로 예외처리이다. 개발자와 사용자의 시각차이는 분명 존재하고 항상 개발자가 의도한대로 사용자가 프로그램을 사용할 수는 없다. 난 개인적으로 완벽한 개발을 할 수 없는 사람이기때문에 여러가지 예외상황이 일어날 것은 항상 염두해두며 작업을 진행한다.

평소와 다를 것 없이 일하고 있는 와중에 내가 작성한 코드를 보니 try catch 가 너무나 남발되고 있다는 느낌을 받았다. 그래서 이번에는 @ControllerAdvice 어노테이션과 @ExceptionHandler 어노테이션을 이용해 컨트롤러단에서 발생되는 예외상황을 캐치하여 공통적으로 처리하고 코드를 조금 더 깔끔하고 간결하게 작성할 수 있는 부분에 대해서 글을 작성해보고자 한다.

예제는 지난번에 글을 작성하며 작성했던 사용자 인증 서비스 코드를 가지고 작성해보았다.

Login Controller

@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);
}

Login Service

@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)
  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에 대한 처리가 가능해진 것이다.

Login Controller

@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);
}

Login Service

@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;
}

이런 식으로 컨트롤러는 그저 전달과 응답에 대한 부분만 수행하면 되고, 서비스는 각 예외상황에 맞는 예외를 발생시키며 비즈니스 로직을 수행하면 된다. 그럼 예외처리는 모두 어드바이스 내에서 처리하게 된다.

솔직히 처음에는 큰 차이를 못 느꼈다. 코드가 길면 길어질수록 찾기 힘들어지고 블록은 더더욱 늘어나게 될 상황이 줄어드는 것을 직접 체감하다 보니 앞으로 스프링 프로젝트를 새로 시작하게 되더라도 예외처리는 기본적으로 공통 처리로 세팅해놓고 작업하게 될 것 같다.

profile
놀고싶다

0개의 댓글