[Spring] 스프링에서 API 예외(오류) 처리하기 (@ExceptionHandler, @ControllerAdvice)

@ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공합니다.

 

사용방법은, 잡을 예외를 파라미터로 갖는 메서드를 만들고 메서드 위에 @ExceptionHandler 애노테이션을 선언하여 사용합니다.

해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 해당 컨트롤러에서 예외가 발생할 경우 이 메서드가 호출됩니다.

(발생한 예외를 잡으면 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡습니다.)

 

에러 코드와 메세지를 갖는 객체

오류가 발생하면 단순하게 오류 코드와 메시지를 JSON 데이터로 보내는 단순한 구조를 갖는 예제로 설명하겠습니다.

@Data
@AllArgsConstructor
public class ErrorResult {
    private String code;
    private String message;
}

 

Member 객체
@Data
@AllArgsConstructor
static class Member {
    private String memberId;
    private String name;
}

 

API 예외 컨트롤러

@ExceptionHandler 에 예외를 지정할 수도 있고, 생략할 수 있습니다. 생략하면 메서드 파라미터의 예외가 지정됩니다.

@ResponseStatus 어노테이션을 선언하여 그 옵션에 표시하고 싶은 HTTP 상태코드를 지정할 수 있습니다.

@RestController
public class ApiExceptionController {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        return new ErrorResult("BAD_INPUT", e.getMessage());
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    // Exception은 최상위 예외로 처리하지 않은 예외가 발생할 시 여기서 예외를 처리해 ErrorResult를 반환한다.
    public ErrorResult exHandle(Exception e) {
        return new ErrorResult("SERVER_EXCEPTION", "내부 오류 발생");
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler
    public ErrorResult userExHandler(NullPointerException e) {
        return new ErrorResult("NULL_EXCEPTION", e.getMessage());
    }

    @GetMapping("/api2/members/{id}")
    public Member getMember(@PathVariable("id") String id) {
        if (id.equals("qeqeqe")) {
            throw new RuntimeException("알수 없는 문제가 발생했습니다.");
        }

        if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 입력 값을 입력했습니다.");
        }

        if (id.equals("null")) {
            throw new NullPointerException("공백을 입력할 수 없습니다.");
        }

        return new Member(id, "반갑습니다." + id + "님");
    }
}

 

요청 파라미터 경로인 id 값을 위에서 정의한대로 보냈을 경우 각각의 응답결과는 다음과 같습니다.

  • http://localhost:8080/api2/members/qeqeqe

RuntimeException을 잡는 @ExceptionHandler 가 없기 때문에 이 예외의 부모인 Exception을 잡은 @ExceptionHandler가 호출됩니다.

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 으로 HTTP 상태코드가 500번인 것을 볼 수 있습니다.

 

 

  • http://localhost:8080/api2/members/bad

IllegalArgumentException을 잡는 @ExceptionHandler가 호출됩니다.

@ResponseStatus(HttpStatus.BAD_REQUEST) 으로 HTTP 상태코드가 400번인 것을 볼 수 있습니다.

* 우선 순위 *
스프링의 우선순위는 항상 자세한 것이 우선권을 가집니다.
여기서 예외의 최상위 부모인 Exception을 잡는 @ExceptionHandler가 있지만
그 자식이 더 자세한 것이므로 여기서는 IllegalArgumentException를 잡는 @ExceptionHandler가 호출됩니다.

 

  • http://localhost:8080/api2/members/null

위와 같은 경우로 Exception이 아닌  NullPointerException을 잡는 @ExceptionHandler가 호출됩니다.

@ResponseStatus(HttpStatus.BAD_REQUEST) 으로 HTTP 상태코드가 400번인 것을 볼 수 있습니다.

 


@ControllerAdvice

@ExceptionHandler를 이용해 예외를 깔끔하게 처리할 수 있었습니다.

그런데, 문제점이있습니다.

바로 정상 처리 코드와 예외 처리 코드가 하나의 컨트롤러에 같이 있다는 것입니다.

 

이 문제점을 @ControllerAdvice 또는 @RestControllerAdvice 를 사용하여 둘을 분리해 문제점을 해결 할 수 있습니다.

 

Controller
@RestController
public class ApiExceptionController {

    @GetMapping("/api2/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("qeqeqe")) {
            throw new RuntimeException("알수 없는 문제가 발생했습니다.");
        }

        if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 입력 값을 입력했습니다.");
        }

        if (id.equals("null")) {
            throw new NullPointerException("공백을 입력할 수 없습니다.");
        }

        return new MemberDto(id, "반갑습니다." + id + "님");
    }
}

 

API 예외 처리 ControllerAdvice
@Slf4j
@RestControllerAdvice(annotations = RestController.class) // @RestController에만 적용 
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        return new ErrorResult("BAD_INPUT", e.getMessage());
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    // Exception은 최상위 예외로 처리하지 않은 예외가 발생할 시 여기서 예외를 처리해 ErrorResult를 반환한다.
    public ErrorResult exHandle(Exception e) {
        return new ErrorResult("SERVER_EXCEPTION", "내부 오류 발생");
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler
    public ErrorResult userExHandler(NullPointerException e) {
        return new ErrorResult("NULL_EXCEPTION", e.getMessage());
    }
}

 

@RestControllerAdvice를 사용하여 컨트롤러에 더 이상 예외를 처리하는 코드가 없이 깔끔하게 분리가 된 것을 볼 수 있습니다.

(@RestControllerAdvice 는 @ControllerAdvice 와 같고, @ResponseBody 가 추가된 것)

 

위 에서 @RestControllerAdvice의 대상으로 RestController을 지정하였는데, @RestController 어노테이션이 있는 대상에만 적용한다는 뜻입니다.

이렇게 특정 애노테이션이 있는 컨트롤러를 지정할 수 있고, 특정 패키지, 특정 클래스도 직접 지정할 수도 있습니다.

패키지 지정의 경우 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 된다.

 

만약, @ControllerAdvice또는 @RestControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용이 됩니다. (글로벌 적용)