예외 처리

Hyun·2024년 4월 5일
0

Spring

목록 보기
38/38
post-thumbnail

스프링이 제공하는 다양한 예외 처리 방법

자바에서는 예외 처리를 위해 try-catch 를 사용하지만, try-catch 를 모든 코드에 붙이는 것은 비효율적이다. 따라서 스프링은 에러 처리라는 공통 관심사를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안했다. 그리하여 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다.

HandlerExcpetionResolver

HandlerExcpetionResolver 는 발생한 Exception 을 catch 하고 HTTP 상태나 응답 메시지 등을 설정한다. 따라서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식된다.

public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

다음은 HandlerExcpetionResolver 인터페이스의 우선순위에 따른 4가지 구현체이다. 이 구현체들은 스프링 빈으로 등록되어 있다. 에러가 발생하면 스프링은 적용 가능한 구현체를 찾아 예외 처리를 한다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

스프링은 아래와 같은 도구들을 이용해 ExceptionResolver 를 동작시켜 에러를 처리할 수 있다.

  • ResponseStatus
  • ResponseStatusException
  • ExceptionHandler
  • ControllerAdvice, RestControllerAdvice

@ResponseStatus

@ResponseStatus 는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다. 즉 HTTP 상태 코드를 변경할 수 있다. @ResponseStatus 는 다음과 같은 경우들에 적용할 수 있다.

  • Exception 클래스 자체
  • 메서드에 @ExceptionHandler 와 함께
  • 클래스에 @RestController 와 함께

위 경우 중 메서드에 @ExceptionHandler 와 함께 사용하는 경우가 일반적이다. @ExceptionHandler 에 대해 알아보자

@ExceptionHandler

@ExceptionHandler 는 매우 유연하게 에러를 처리할 수 있는 방법을 제공한다. @ExceptionHandler 는 @ResponseStatus 와 달리 에러 응답(payload)를 자유럽게 다룰 수 있다.

@ExceptionHandler 는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.

  • 컨트롤러의 메서드(해당 클래스 내에서 발생하는 예외를 처리)
  • @ControllerAdvice 나 @RestControllerAdvice 가 있는 클래스의 메서드(전역적으로 예외를 처리, Rest 가 붙으면 JSON으로 반환)

정리를 해보자

HandlerExcpetionResolver 는 발생한 Exception 을 catch 하고 HTTP 상태나 응답 메시지 등을 설정하는 인터페이스이다.

그리고 해당 인터페이스의 대표적인 구현체로 ExceptionHandlerExceptionResolver 가 있다.

ExceptionHandlerExceptionResolver 는 @ExceptionHandler 가 특정 Exception 클래스를 속성으로 받아 예외를 처리할 수 있게 한다.

@ExceptionHandler 가 사용될 수 있는 위치는 컨트롤러의 메서드 or @ControllerAdvice 나 @RestControllerAdvice 가 있는 클래스의 메서드이다.

=> @ExceptionHandler 를 메서드와 함께 사용해 에러를 유연하게 처리할 수 있다.

@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

상황별로 발생하는 에러를 유연하게 대처하기 위해 사용자 정의 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 는 아래와 같이 그냥 throw 하면 된다. 그럼 @RestControllerAdvice 의 @ExceptionHandler(UserNameDuplicateException.class) 어노테이션이 해당 에러를 catch 하여 해당 어노테이션이 붙은 메서드 내에서 처리한다.
JoinService.class


유연성 확대 리팩토링

지금의 사용자 정의 Exception 은 모든 에러에 대해 각각의 Exception 클래스를 만드는 방법임
-> 예외 처리가 많아질수록 클래스 개수 무한 증가

유연성 확대 리팩토링 방법

  • 패키지별로 Enum 을 사용하여 패키지별 예외 경우들을 한번에 관리
  • 동일한 패키지에 속하는 에러는 동일한 예외로 처리(예외 발생시킬때, Enum의 예외 코드도 함께 전달)
  • 동일한 예외는 패키지별 @RestControllerAdvice 이 붙은 클래스에서 @ExceptionHandler 로 처리, 이때 예외의 인자로 포함된 예외 코드를 사용해 예외들을 구분하여 처리
  • 반환 body 값 데이터 타입 통일(ErrorResponse 클래스 생성해 사용)

패키지 단위로 예외 코드 Enum 생성

에러 코드 인터페이스, 공통 에러 코드 구현 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;
}

패키지 단위로 사용자 정의 Exception 을 생성

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

패키지 단위로 @RestControllerAdvice 클래스 생성

내부에서 예외 발생 시 인자로 전달받은 예외 코드를 이용해 예외들을 구분하여 처리한다.
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

profile
better than yesterday

0개의 댓글