스프링/스프링 MVC 패턴

스프링 API 예외처리 - @ExceptionHandler (대부분 쓰임)

chanhee01 2023. 3. 5. 15:41

API 예외처리의 어려운 점

- 특정 컨트롤러에서만 발생하는 예외를 처리하기 어렵다.

- 회원 처리하는 컨트롤러에서 발생한 RuntimeException 예외와 상품을 관리하는 컨트롤러에서 발생한 RuntimeException 예외를 서로 다른 방식으로 처리하고 싶다면??

 

이러한 단점들을 보완하기 위해서 @ExceptionHanlder를 사용하면 된다.

 

@Slf4j
@RestController
public class ApiExceptionV2Controller {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @GetMapping("/api2/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {

        if (id.equals("ex")) {
            throw new RuntimeException("잘못된 사용자");
        }
        if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 입력 값");
        }
        if (id.equals("user-ex")) {
            throw new UserException("사용자 오류");
        }

        return new MemberDto(id, "hello " + id);
    }

    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String memberId;
        private String name;
    }
}

위의 ExceptionHandler 애노테이션을 제외한 메서드들은 이전의 코드를 복사해 온 것이다.

@ExceptionHanldler 애노테이션을 사용하면 해당 에러를 잡아서 예외를 처리하게 된다. 하지만 200 Status로 예외 없이 처리되기 때문에 @ResponseStatus로 400 에러가 나오게 만들어줬다.

 

 

 

@ExceptionHandler // 여기 안하고 메서드 파라미터에다가 줘도 됨
public ResponseEntity<ErrorResult> userExHandler(UserException e) {
    log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
}

UserException 예외를 처리하는 방식이다. 애노테이션의 정보를 생략하고 메서드의 파라미터에다가 해당 예외 상태를 넣어두었다.

 

 

 

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e) {
    log.error("[exceptionHandler] ex", e);
    return new ErrorResult("EX", "내부 오류");
}

Exception은 모든 예외 처리의 부모 클래스이다. 위의 2개는 이미 예외 처리가 있어서 2개의 예외는 저 코드가 실행되게 된다. 하지만 저 2개의 예외를 제외하고 처리하지 못한 예외가 발생했을 때에는 Exception이라는 루트 클래스의 에러가 처리하게 된다.

 

 

 

스프링의 우선순위는 항상 자세한 것이 우선순위를 가지기 때문에 부모, 자식 클래스가 다 있으면 자식예외 클래스가 우선으로 처리된다.

 

 

 

 

 

API 예외처리 - @ControllerAdvice (정상, 예외 코드의 분리)

컨트롤러에서 예외 코드만 따로 분리해서 새로운 클래스를 만들었다.

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ExceptionHandler // 여기 안하고 메서드 파라미터에다가 줘도 됨
    public ResponseEntity<ErrorResult> userExHandler(UserException e) {
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }
}

 

이전 코드에서 위의 코드를 가져오고, 이전 코드에서는 위와 같은 에러 코드는 삭제했다.

 

정상 코드와 에러 코드가 붙어있으면 혼란을 줄 수 있기 때문에 위와 같이 정상 코드와 에러 코드 분리를 통해 코드가 더 간결해졌다.

 

 

@ControllerAdvice 라고만 쓰면 모든 컨트롤러에서 전부 사용 가능하다.

@ControllerAdvice(annotaions = RestController.class) - RestController에서만 사용가능

@ControllerAdvice("org.example.controllers") - 특정 패키지에서만 사용가능

@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) - 특정 클래스에서만 사용가능

 

 

보통 3번째 줄을 많이 사용한다고 한다.