개발바닥곰발바닥
728x90

GlobalExceptionHandler와 CustomException으로 전역 예외 처리하기

Spring을 사용해서 웹 애플리케이션 개발을 하다 보면, 예외가 발생했을 때 예외 종류에 따라 클라이언트 측에 다른 응답을 보내줘야 한다.

이때, 일일히 Controller마다 예외 처리해서 Response를 던져주는 작업을 하게 되면 서비스가 커질수록 중복 코드가 늘어나고 유지보수가 힘들어진다.

이를 해결하기 위해 Exception Handling을 전역적으로 관리할 수 있는 방법을 알아보자

 

ErrorCode Enum

에러에 대해서 Response를 전달할 때, 에러 메시지를 같이 전달하게 되는데 이 때 메시지를 String 값으로 하드 코딩하게 되면 관리가 힘들어진다. 그러므로 Http 상태 코드와 메시지를 관리할 수 있는 ErrorCode Enum을 만들어준다.

@Getter
@AllArgsConstructor
public enum ErrorCode {
	//400 BAD_REQUEST
	INVALID_ID(BAD_REQUEST, "유효하지 않은 ID입니다."),
	INVALID_INPUT_VALUE(BAD_REQUEST, "입력 양식과 맞지않는 입력값입니다."),

// 401 UNAUTHORIZED
	INVALID_TOKEN(UNAUTHORIZED, "유효하지 않은 토큰입니다."),

//404 NOT_FOUND
	NOT_FOUND_PROJECT(NOT_FOUND, "프로젝트를 찾을 수 없습니다."),
	NOT_FOUND_IMAGE(NOT_FOUND, "이미지를 찾을 수 없습니다.");

	private final HttpStatus httpStatus;
	private final String detail;
}

위와 같이 Http Status Code별로 에러를 정의하여 Exception을 throw 할 때 공통으로 사용할 수 있도록 한다.

CustomException Class

예외를 던져주기 위해 RuntimeException을 상속받은 CustomException 클래스를 생성하여 ErrorCode를 가질 수 있게 한다.

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
	private final ErrorCode errorCode;
}

ErrorResponse Class

클라이언트 측에 Error에 대한 정보를 전달하기 위해 ErrorResponse 클래스를 정의해준다.

Response에서 어떤 정보들을 전달해줄지는 필요에 따라 정의하면 된다.

@Getter
@Builder
public class ErrorResponse {
	private final LocalDateTime timestamp = LocalDateTime.now();
	private final int status;
	private final String error;
	private final String code;
	private final String message;

	public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
		return ResponseEntity
			.status(errorCode.getHttpStatus())
			.body(ErrorResponse.builder()
				.status(errorCode.getHttpStatus().value())
				.error(errorCode.getHttpStatus().name())
				.code(errorCode.name())
				.message(errorCode.getDetail())
				.build());
	}
}

GlobalExceptionHandler Class

이제 전역적으로 에러를 핸들링할 수 있는 GlobalExceptionHandler 클래스를 만들어 줘야 하는데, 해당 클래스에 @RestControllerAdvice 어노테이션을 붙여 RestController에서 발생하는 예외를 받을 수 있게 해줘야 한다.

 

3개의 메서드 중에서 첫 번째와 두 번째는 Request Parameter에 대한 Validation Exception이 발생했을 때 처리해주는 부분이고, 우리가 아까 만들었던 CustomException을 잡아주는 곳은 handleCustomException 메서드이다.

 

아까 정의한 ErrorCode와 ErrorResponse를 사용해서 클라이언트에 ResponseEntity를 전달하는 것을 볼 수 있다.

 

@RestControllerAdvice
public class GlobalExceptionHandler {
	//Reqeust Body Validation Exception
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException exception) {
		return ErrorResponse.toResponseEntity(ErrorCode.INVALID_INPUT_VALUE,
			exception.getBindingResult().getFieldError().getDefaultMessage());
	}

	//Reqeust Param or ModelAttribute Validation Exception
	@ExceptionHandler(BindException.class)
	public ResponseEntity<ErrorResponse> handleValidationExceptions(BindException exception) {
		return ErrorResponse.toResponseEntity(ErrorCode.INVALID_INPUT_VALUE,
			exception.getBindingResult().getFieldError().getDefaultMessage());
	}

	@ExceptionHandler(CustomException.class)
	public ResponseEntity<ErrorResponse> handleCustomException(CustomException exception) {
		return ErrorResponse.toResponseEntity(exception.getErrorCode());
	}
}

 

throw CustomException

Service 단에서 실제로 CustomException을 발생시키는 부분을 한번 보도록 하겠다.

/**
	 * 회원정보 조회
	 * @param memberId 조회할 대상 회원 ID
	 * @return 회원정보
	 */
	@Transactional(readOnly = true)
	public MemberDto.Info getMemberInfo(Long memberId) {
		Member member = memberRepository.findByIdAndIsFirstIsFalse(memberId)
			.orElseThrow(() -> new CustomException(NOT_FOUND_MEMBER));

		return MemberDto.Info.from(member);
	}

아주 간단한 회원정보를 조회하는 기능이다. 이때 memberRepository에 memberId로 조회를 했을 때, 해당 멤버가 존재하지 않으면 CustomException에 NOT_FOUND_MEMBER ErrorCode를 넣어 throw 해주는 것을 볼 수 있다.

 

이렇게 Service를 거쳐 Controller로 Exception이 전달되게 되면, 위에서 정의했던 GlobalExceptionHandler가 해당 예외를 처리하게 되어 클라이언트는 ErrorResponse를 전달받을 수 있다.

 

 

클라이언트가 전달받는 Response 값

{
    "timestamp": "2022-10-26T23:33:36.7143144",
    "status": 404,
    "error": "NOT_FOUND",
    "code": "NOT_FOUND_MEMBER",
    "message": "회원을 찾을 수 없습니다."
}

이러한 예외 처리 방식은 코드 작성할 때도 효율적이고 실무에서도 비슷한 방식으로 사용한다고 하니 알아두면 좋을 것 같다.👍

참고

https://bcp0109.tistory.com/303

728x90
profile

개발바닥곰발바닥

@bestinu

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!