개발

@ControllerAdvice 와 @ExceptionHandler를 이용한 예외처리

효방 2020. 8. 13. 18:53

개인적으로 평소에 일이나 개인 공부를 할때 가장 신경쓰는 것은 예외처리이다. 

 

개발자와 사용자의 시각차이는 분명 존재하고 항상 개발자가 의도한대로 사용자가 프로그램을 사용할 수는 없다.

 

난 개인적으로 완벽한 개발을 할 수 없는 사람이기때문에 여러가지 예외상황이 일어날 것은 항상 염두해두며 작업을

 

진행한다. 그래서 예외처리를 굉장히 중요하게 생각한다. 어떠한 상황이 일어날지 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;
}

 

이런 식으로 컨트롤러는 그저 전달과 응답에 대한 부분만 수행하면 되고, 서비스는 각 예외상황에 맞는 예외를

 

발생시키며 비즈니스 로직을 수행하면 된다. 그럼 예외처리는 모두 어드바이스 내에서 처리하게 된다.

 

솔직히 처음에는 큰 차이를 못 느꼈다. 코드가 길면 길어질수록 찾기 힘들어지고 블록은 더더욱 늘어나게 될 상황이

 

줄어드는 것을 직접 체감하다 보니 앞으로 스프링 프로젝트를 새로 시작하게 되더라도 예외처리는 기본적으로

 

공통 처리로 세팅해놓고 작업하게 될 것 같다.