본문 바로가기

Web Sever 개발과 CS 기초/스프링

@ExceptionHandler, @ControllerAdvice를 사용한 예외를 원하는 Response 처리

개요와 목적

스프링 REST API 서버에 정상적인 요청이 오면, 예외 발생 없이, 2XX Status Code와 api 정보를 제공한다.

하지만 잘못된 요청이 왔을 때(잘못된 파라미터 타입 등)는 예외가 발생하고, 그 예외를 catch하여 그에 맞는 Response header, body를 전송한다.

모든 예외를 Controller영역에서 try - catch하여 Response를 설정하면, 코드가 굉장히 복잡해지고, 유지 보수성이 떨어진다.

그래서 예외 발생 시, 스프링의 ExceptionResolver(@ExceptionHandler)를 사용해, 원하는 Response를 반환하는 방법에 대해서 알아보자.

Spring이 제공하는 ExceptionResolver 종류 

ExceptionResolver가 없다면,

예외 발생 -> 서블릿 -> WAS까지 예외가 전달 되어서 500으로 상태 코드 전달된다.

ExceptionResolver를 설정하면,

예외 발생 -> 서블릿 -> ExceptionResolver에서 예외를 잡아서 어떤 식으로 처리를 할 지 선택권이 생긴다..

ExceptionResolver 단계

스프링에서 예외가 발생하면, 아래에서 설명할 1,2,3 단계 Resolver가 순서대로 호출되어 Exception을 처리한다.

만약 1단계 Resolver에서 null을 반환하면, 다음 2단계 Resolver가 호출되어 정상 응답 처리를 시도하는 형태이다.

1단계 ExceptionHandlerExceptionResolver

@ExceptionHandler를 사용해 처리를 한다. 가장 세밀하게 예외에 따른 Response를 처리할 수 있고, 다음 챕터에서 실제 예제와 함께 사용 방법에 대해서 알아본다.

2단계 ResponseStatusExceptionResolver

@ResponseStatus 가 달려있는 예외 또는 ResponseStatusException 예외

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
//BadRequestException 예외가 컨트롤러 밖으로 넘어가면 
//ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서
//오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경한다.

3단계 DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.

예를 들어, 파라미터 바인딩 예외(TypeMismatchException)가 발생하면, 스프링이 미리 설정 해 놓은, DefaultHandlerExceptionResolver.handleTypeMismatch를 통해서 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.

스프링 내부 오류를 어떻게 처리할지 DefaultHandlerExceptionResolver에 많은 내용이 정의되어 있다.

@ExceptionHandler를 사용해서 예외 메소드 분리하기

@ExceptionHandler을 사용하면, 1단계인 ExceptionHandlerExceptionResolver를 사용하게 된다. 가장 세밀하게 예외를 처리할 수 있고 실무에서 대부분 이 Resolver를 사용한다.

try - catch까지 포함된 Controller 코드를 @ExcptionHandler를 사용해 예외를 분리한 깔끔한 코드로 바꾸면서 사용법을 알아보자.

(예시, 음식 가격을 업데이트하는 메소드에서 Request에 공백이 들어오면 예외가 발생하고 그에 맞는 Response를 반환한다.)

package apideliveryservice.controller;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/delivery-service/company")
public class CompanyFoodController {

    private final CompanyFoodService companyFoodService;

    @PutMapping("/food/update")
    public ResponseEntity updatePrice(@RequestBody RequestCompanyFoodPriceDto request) 
				throws SQLException {
        try {
            companyFoodService.updatePrice(request.getFoodId(), request.getPrice());
	    //예외 없이 성공햇을 때 Response
            ResponseCompanyFoodSuccess success = new ResponseCompanyFoodSuccess(200, null, null);
            return ResponseEntity.status(HttpStatus.OK).body(success);
	//공백이 들어온 예외 발생 catch로 잡아서 원하는 response 반환
        } catch (BlackException e) {
            log.info("ex", e);
            ResponseCompanyFoodError error = new ResponseCompanyFoodError(
                "/errors/food/update/black-input"
                , "BlackException", 400, "update food price fail due to black request input"
                , "/api/delivery-service/company/food/update");
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
        } 
    }
}

@ExceptionHandler해서 Exception - Response 반환 코드 분리하기

package apideliveryservice.controller;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/delivery-service/company")
public class CompanyFoodController {

    private final CompanyFoodService companyFoodService;
		
    //어노테이션에 해당 예외 클래스를 지정하고, 반환할 리스폰스 처리를 지정한다.
    @ExceptionHandler(BlackException .class)
    public ResponseEntity blackExceptionExHandle(BlackException e) {
        ResponseCompanyFoodError error = new ResponseCompanyFoodError(
                "/errors/food/update/black-input"
                , "BlackException", 400, "update food price fail due to black request input"
                , "/api/delivery-service/company/food/update");
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    @PutMapping("/food/update")
    public ResponseEntity updatePrice(@RequestBody RequestCompanyFoodPriceDto request)
        throws SQLException {
        //컨트롤러 메소드에는 성공된 경우만 코드로 작성하면 된다.
        companyFoodService.updatePrice(request.getFoodId(), request.getPrice());
        ResponseCompanyFoodSuccess success = new ResponseCompanyFoodSuccess(200, null, null);
        return ResponseEntity.status(HttpStatus.OK).body(success);
    }
}

추가적 사용 방법

  • 우선 순위 설정하기

스프링의 우선순위는 항상 자세한 것이 우선권을 가진다. 예를 들어서 부모, 자식 클래스가 있고 다음과 같이 예외가 처리된다.

@ExceptionHandler(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
  • 다양한 예외

다양한 예외를 하나의 @ExceptionHandler에 처리할 수 있다.

@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
 log.info("exception e", e);
}
  • 예외 생략

@ExceptionHandler 에 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {}

@ControllerAdvice를 사용해서 깔끔하게 클래스 분리하기

@ControllerAdvice 또는 @RestControllerAdvice 를 사용하면 정상 코드와 예외 처리 코드의 클래스를 분리할 수 있다.

해당 어노테이션을 사용해서 위 공백 예외 처리를 다른 클래스로 분리해보자.

package apideliveryservice.controllerExceptionAdvice;

//적용할 패키지, 클래스 등을 설정할 수 있다. 
@RestControllerAdvice(assignableTypes = {CompanyFoodController.class})
public class CompanyFoodControllerExceptionAdvice {

    @ExceptionHandler(BlackException .class)
    public ResponseEntity blackExceptionExHandle(BlackException e) {
        log.error("[exceptionHandle] ex", e);

        ResponseCompanyFoodError error = new ResponseCompanyFoodError(
                "/errors/food/update/black-input"
                , "BlackException", 400, "update food price fail due to black request input"
                , "/api/delivery-service/company/food/update");
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
}

//컨트롤러 클래스에는 성공 관련 메소드만 남아있게 된다.

추가적 사용법

  • @ControllerAdvice 는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler , @InitBinder 기능을 부여해주는 역할을 한다.
  • @ControllerAdvice 에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)
  • @RestControllerAdvice 는 @ControllerAdvice 와 같고, @ResponseBody 가 추가되어 있다

Response

https://jeong-pro.tistory.com/195

인프런 김영한 MVC 강의