Exception의 기본 개념 : https://dmansp.tistory.com/39
왜 알아보았는가
clean code를 읽어보면 try-catch를 통한 예외처리는 최대한 지양하라는 의견이 지배적입니다. 서비스 로직을 작성할 때 throw하는 모든 exception에 대해 try-catch로 주렁주렁 예외처리를 해주면 적절한 관심사 분리가 이루어지지 않습니다. 이는 명백한 SRP 위반이며 매우 복잡한 코드가 되어 유지보수 하기 어려워집니다. 따라서 코드는 아래처럼 작성하는 것이 clean한 코드입니다.
function registerUser()
{
var request = receiveRequest();
validateRequest(request);
canonicalizeEmail(request);
updateDbFromRequest(request);
sendEmail(request.Email);
return sendResponse("Success");
}
서비스에서 사용자를 등록할 때 어떠한 과정을 거치는 지 누구든 한 눈에 알 수 있습니다. 그러면 각 과정에서 throw하는 exception은 어디서 예외 처리를 해줘야할까요? 바로 Exception Handler를 통해 따로 관리해줍니다.
@ExceptionHandler
해당 Annotation을 붙이면 value로 주는 Exception Class에 대해 예외 처리를 해줄 수 있습니다.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException : {}", e);
final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INVALID_METHOD_ARGUMENT);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
}
(MethodArgumentNotValidException
는 보통 request로 받은 data에 대해 사용자가 정의한 유효성 검증에 실패한 경우 발생합니다. 궁금하면 @Valid를 알아보세요!)
위 코드를 예시로 들면, MethodArgumentNotValidException
이 발생했을 때 handleMethodArgumentNotValidException()
를 호출합니다. 이후 에러 로그를 찍고, ResponseEntity의 형태로 값을 반환합니다.
이처럼 Exception Handler를 이용한다면 핵심적인 비즈니스 관심사와 예외 처리에 대한 관심사를 분리할 수 있습니다.
@ControllerAdvice
Controller에서 발생하는 exception들을 handler로 잡아준다고 명시적으로 지정하는 annotation입니다. 속성을 설정해서 특정 패키지, 혹은 특정 컨트롤러를 한정할 수도 있습니다.
일반적으로 스프링 프로젝트에서는 컨트롤러에서 발생할 수 있는 Exception을 범용적으로 처리해주기 위해 GlobalExceptionHandler
를 만듭니다.
아래 코드를 참고 합시다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException : {}", e);
final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INVALID_METHOD_ARGUMENT);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponseDto> handleBusinessException(BusinessException e) {
log.warn("BusinessException : {}", e.getMessage());
final ErrorResponseDto response = ErrorResponseDto.of(e.getResultCode());
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
}
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponseDto> handleException(Exception e) {
log.error("handleException", e);
final ErrorResponseDto response = ErrorResponseDto.of(ResultCode.INTERNAL_SERVER_ERROR);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatusCode()));
}
}
FLOW
- User Defined Exception Class 생성
- hadling 할 메소드 생성 후 @ExceptionHandler 어노테이션 추가 후 해당 Exception을 속성값으로 넣어줌
- 해당 Exception 발생 시 핸들러에서 어떤 로직 처리해줄 건지 코드 작성
- ErrorCode 포함해서 ResponseEntity에 넣어주고 해당 객체 return 해주도록 함
- 이 때 return 해주는 ResponseEntity 객체의 타입은 일정하게 response해주는 게 좋음!
- 위 과정 적용하면 이후에 서비스 로직에서 throw new UserDefinedException() 던지면 Handler에서 알아서 처리해줌!
참고1. 비즈니스 예외처리
비즈니스적인 예외처리는 아래 상속구조 따르면 통일성있게 처리 가능

참고2. ErrorCode
ErrorCode를 관리할 수 있는 ENUM 생성 후 따로 관리해주기
- ErrorCode Enum 생성
- Exception 상속 받는 User Defined Exception의 생성자 인자로 넣어주기
참고
https://github.com/cheese10yun/spring-guide/blob/master/docs/exception-guide.md#exception-guide
https://velog.io/@kiiiyeon/스프링-ExceptionHandler를-통한-예외처리
https://kwangyulseo.com/2015/05/29/관심사의-분리separation-of-concerns/