자바에서는 예외 처리를 위해 try-catch 를 사용하지만, try-catch 를 모든 코드에 붙이는 것은 비효율적이다. 따라서 스프링은 에러 처리라는 공통 관심사를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안했다. 그리하여 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다.
HandlerExcpetionResolver 는 발생한 Exception 을 catch 하고 HTTP 상태나 응답 메시지 등을 설정한다. 따라서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식된다.
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
다음은 HandlerExcpetionResolver 인터페이스의 우선순위에 따른 4가지 구현체이다. 이 구현체들은 스프링 빈으로 등록되어 있다. 에러가 발생하면 스프링은 적용 가능한 구현체를 찾아 예외 처리를 한다.
스프링은 아래와 같은 도구들을 이용해 ExceptionResolver 를 동작시켜 에러를 처리할 수 있다.
@ResponseStatus 는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다. 즉 HTTP 상태 코드를 변경할 수 있다. @ResponseStatus 는 다음과 같은 경우들에 적용할 수 있다.
위 경우 중 메서드에 @ExceptionHandler 와 함께 사용하는 경우가 일반적이다. @ExceptionHandler 에 대해 알아보자
@ExceptionHandler 는 매우 유연하게 에러를 처리할 수 있는 방법을 제공한다. @ExceptionHandler 는 @ResponseStatus 와 달리 에러 응답(payload)를 자유럽게 다룰 수 있다.
@ExceptionHandler 는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.
정리를 해보자
HandlerExcpetionResolver 는 발생한 Exception 을 catch 하고 HTTP 상태나 응답 메시지 등을 설정하는 인터페이스이다.
그리고 해당 인터페이스의 대표적인 구현체로 ExceptionHandlerExceptionResolver 가 있다.
ExceptionHandlerExceptionResolver 는 @ExceptionHandler 가 특정 Exception 클래스를 속성으로 받아 예외를 처리할 수 있게 한다.
@ExceptionHandler 가 사용될 수 있는 위치는 컨트롤러의 메서드 or @ControllerAdvice 나 @RestControllerAdvice 가 있는 클래스의 메서드이다.
=> @ExceptionHandler 를 메서드와 함께 사용해 에러를 유연하게 처리할 수 있다.
@ExceptionHandler 의 대표적인 사용 형식은 메서드 + @ExceptionHandler + 반환 타입으로 Response Entity 사용
Response Entity 를 사용하면 .status 메서드를 통해 HTTP 상태 코드를 변경할 수 있다. 따라서 @ResponseStatus 를 사용할 필요가 없다.
*주의) @ResponseStatus or Response Entity 사용 없이 그냥 객체 반환 시 필드에 HttpStatus 값이 있더라도 클라이언트는 자동으로 읽는게 안됨 -> 그냥 Response Entity를 사용하고 .status 로 상태 코드를 설정하자
예시)
@ExceptionHandler(UserNameDuplicateException.class)
public ResponseEntity<Map<String, String>> userNameDuplicateExceptionHandler(UserNameDuplicateException exception){
Map<String, String> errorMap = new HashMap<>();
errorMap.put("errorMessage", exception.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorMap);
}
위 코드는 @RestController 가 붙은 클래스 내 메서드에 @ExceptionHandler 어노테이션을 사용하였다. 속성의 UserNameDuplicateException.class 타입의 Exception 이 발생한 경우 해당 에러를 catch 하여 메서드 내에서 처리한다.
상황별로 발생하는 에러를 유연하게 대처하기 위해 사용자 정의 Exception 클래스를 사용한다.
예를 들어 @RestControllerAdvice 클래스 내 @ExceptionHandler(RuntimeException.class)는 전역으로 발생하는 모든 RuntimeException 예외를 catch 한다. 따라서 상황별로 발생하는 RuntimeException 에 대한 처리를 구분할 수 없다. 그러므로 Exception 을 상속받는 구체적인 상황에 대한 예외 클래스를 생성하여 상황별 에러를 유연하게 처리할 수 있도록 한다.
*Exception 클래스를 상속받은 클래스가 super() 를 호출하면 인자가 exception 의 message 속성에 입력된다.
UserNameDuplicateException.class
public class UserNameDuplicateException extends RuntimeException {
//Exception 객체의 message 필드에 메시지가 담김
public UserNameDuplicateException(String username){
super(username + " already exists :");
}
}
에러 throw 는 아래와 같이 그냥 throw 하면 된다. 그럼 @RestControllerAdvice 의 @ExceptionHandler(UserNameDuplicateException.class) 어노테이션이 해당 에러를 catch 하여 해당 어노테이션이 붙은 메서드 내에서 처리한다.
JoinService.class
지금의 사용자 정의 Exception 은 모든 에러에 대해 각각의 Exception 클래스를 만드는 방법임
-> 예외 처리가 많아질수록 클래스 개수 무한 증가
유연성 확대 리팩토링 방법
에러 코드 인터페이스, 공통 에러 코드 구현 Enum
// ErrorCode 인터페이스
public interface ErrorCode {
String name();
HttpStatus getHttpStatus();
String getMessage();
}
// CommonErrorCode 구현 클래스
@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCode{
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "유효하지 않은 파라미터가 포함되어 있습니다."),
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "리소스가 존재하지 않습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 에러 발생"),
;
private final HttpStatus httpStatus;
private final String message;
}
User 에러 코드 구현 Enum
// UserErrorCode 구현 클래스
@Getter
@RequiredArgsConstructor
public enum UserErrorCode implements ErrorCode {
NOT_FOUND_USER(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."),
INACTIVE_USER(HttpStatus.FORBIDDEN, "유저가 현재 비활성화 상태입니다."),
CONFLICT_USER(HttpStatus.CONFLICT,"해당 닉네임이 이미 존재합니다.");
private final HttpStatus httpStatus;
private final String message;
}
Board 에러 코드 구현 Enum
@Getter
@RequiredArgsConstructor
public enum BoardErrorCode {
NOT_FOUND_BOARD(HttpStatus.NOT_FOUND, "해당 게시글을 찾을 수 없습니다.");
private final HttpStatus httpStatus;
private final String message;
}
User 패키지의 사용자 정의 Exception
@Getter
@RequiredArgsConstructor
public class UserApiException extends RuntimeException {
private final ErrorCode errorCode;
}
Board 패키지의 사용자 정의 Exception
@Getter
@RequiredArgsConstructor
public class BoardApiException extends RuntimeException {
private final ErrorCode errorCode;
}
내부에서 예외 발생 시 인자로 전달받은 예외 코드를 이용해 예외들을 구분하여 처리한다.
User 패키지의 @RestControllerAdvice 가 붙은 클래스
@RestControllerAdvice // 응답(에러 메시지)을 JSON 형식으로 반환
public class UserExceptionAdvice {
@ExceptionHandler(UserApiException.class)
public ResponseEntity<Object> handleCustomUserException(UserApiException e){
ErrorCode errorCode = e.getErrorCode();
return handleExceptionInternal(errorCode);
}
private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
private ErrorResponse makeErrorResponse(ErrorCode errorCode) {
return ErrorResponse.builder()
.name(errorCode.name())
.message(errorCode.getMessage())
.build();
}
}
반환되는 Response Entity 의 body 에 담을 클래스를 생성하고, 인스턴스에 에외의 이름, 메시지 등을 담아 클라이언트로 전달한다.
@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse { // 에러 응답 클래스
private final String name;
private final String message;
}
예외 처리 시 해당되는 패키지 예외를 발생시키고, 인자로 구체적인 상황을 나타내는 Enum값을 전달한다. 이로써 하나의 @RestControllerAdvice 클래스에서 다양한 상황에 대해 유연한 예외처리가 가능해졌다.
예시)
JoinService.class -> User 에 관한 예외처리
public User joinProcess(JoinDto joinDto) throws Exception {
String username = joinDto.getUsername();
String name = joinDto.getName();
String password = joinDto.getPassword();
String email = joinDto.getEmail();
String role = joinDto.getRole();
Boolean isExist = userRepository.existsByUsername(username);
//동일한 username의 회원이 존재할때
if(isExist){
throw new UserApiException(UserErrorCode.CONFLICT_USER);
}
}
UserController -> User 에 관한 예외처리
@PutMapping("updateUser/{id}")
User updateUser(@RequestBody User newUser,@PathVariable(name = "id") Long id){
return userRepository.findById(id)
.map(user ->{
user.setUsername(newUser.getUsername());
user.setEmail(user.getEmail());
user.setName(newUser.getName());
user.setPassword(user.getPassword());
user.setRole("ROLE_USER");
return userRepository.save(user);
}).orElseThrow(()->new UserApiException(UserErrorCode.NOT_FOUND_USER));
}
참고 자료 및 출처
https://mangkyu.tistory.com/205