[Spring] 스프링 AOP와 @RestControllerAdvice를 이용하여 API 예외 처리(execution, @ExceptionHandler 등)

안녕하세요. 오늘은 최초 프로젝트 구조를 잡을 때, 에러처리가 굉장히 중요한데요.

 

자바에서 에러처리는 try/catch 문을 활용하거나 throws Exception과 같은 문법을 활용하는데요.

 

스프링에서 관점지향 프로그래밍인 AOP와 @RestControllerAdvice, @ExceptionHandler를 이용하여 전체적인 에러처리하는 방법에 대해서 공유하고자 합니다.

 

구조는 아래와 같이 만들어보았습니다.

 

프로젝트 구조

 

Request 요청이 지나가는 순서대로 소스를 살펴보겠습니다.

 

1. RestControllerAspect.java

@Aspect
@Component
public class RestControllerAspect {

    /**
     * Controller 클래스 프록시
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.cowork.dutystem.controller.*.*(..))")
    public SuccessResponse<Object> restResponseHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        return new SuccessResponse<>(HttpStatus.OK.value(), "request success", proceedingJoinPoint.proceed());
    }
}

- @Aspect 어노테이션으로 aop 기능을 사용할 수 있는 클래스입니다. @Component는 이 클래스를 Bean으로 스프링 빈 컨테이너에서 관리하도록 합니다.

- @Aroundcom.cowork.dutystem.controller라는 패키지에 존재하는 모든 클래스와 모든 메소드에 대해서 둘러 싼다고 생각하시면 됩니다. proceddingJoinPoint.proceed()를 호출하게 되면 실제 controller api로 향하게 됩니다.

 

2. TestController.java

@RequiredArgsConstructor
@RequestMapping("/api/v1/test")
@RestController
public class TestController {

    private final TestService testService;

    @GetMapping
    public Object getTest() throws Exception {
        return testService.getTest();
    }

}

- 평범한 RestController로 서비스 로직으로 향합니다.

 

3. ServiceExceptionAspect.java

@Component
@Aspect
public class ServiceExceptionAspect {

    @Around("execution(* com.cowork.dutystem.service.*.*(..))")
    public Object serviceExceptionHandler(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            return proceedingJoinPoint.proceed();
        } catch(Throwable e) {
            throw new RuntimeException(e.getMessage());
        }
    }

}

- 위의 RestControllerAdvice 클래스와 마찬가지로 @Around 어노테이션을 이용하여 Service 로직을 에워싸도록 합니다. 

- 먼저 ServiceAspect 실행되고 proceeding()이 실행되며 TestService의 메소드가 실행되게 됩니다.

- 여기에 try/catch 문이 Throwable 을 바라보고 있는 이유는 서비스에서 throws 던져지는 모든 에러를 잡기 위함입니다. Throwable 은 Exception 보다 상위 클래스이므로 모든 예외를 잡습니다. -> 그리고 이를 RuntimeException으로 다시 던집니다.

 

4. TestService.java

@Service
public class TestService {

    public Object getTest() throws Exception {
        throw new Exception("에러 발생");
    }

}

- 평범한 Service 로직입니다. 근데 에러가 발생했습니다.

- 서비스에서는 Exception을 던지므로 이제 다시 ServiceExceptionAspect 로 흐름이 바뀝니다.

 

다시 ServiceExceptionAspect

@Component
@Aspect
public class ServiceExceptionAspect {

    @Around("execution(* com.cowork.dutystem.service.*.*(..))")
    public Object serviceExceptionHandler(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            return proceedingJoinPoint.proceed();
        } catch(Throwable e) {
            throw new RuntimeException(e.getMessage());
        }
    }

}

ServiceExceptionAspect 클래스에서 RuntimeException이 발생하였고 이는 다시 TestController로 흐름이 바뀌게 됩니다.

 

다시 TestController

@RequiredArgsConstructor
@RequestMapping("/api/v1/test")
@RestController
public class TestController {

    private final TestService testService;

    @GetMapping
    public Object getTest() throws Exception {
        return testService.getTest();
    }

}

여기서는 getTest() 에서 에러가 발생했으니 throws Exception에 의해서 예외가 던져지고 이를 @RestControllerAdvice에 정의해놓은 @ExceptionHandler가 잡게 됩니다.

 

5. RestControllerExceptionAdvice.java

@Slf4j
@RestControllerAdvice
public class RestControllerExceptionAdvice {

    /**
     * 런타임 에러 -> 서비스 에러 처리
     * @param e
     * @param req
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(RuntimeException.class)
    public ExceptionResponse handlerRuntimeException(RuntimeException e, HttpServletRequest req) {
        log.error("==================== handlerRuntimeException ====================");
        e.printStackTrace();
        return new ExceptionResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null);
    }
}

미리 정의된 ExceptionHandler에 의해서 handleRuntimeException 메소드를 타고 예외 처리된 객체를 통해 Bad Request 코드인 400을 던지고 예외 메시지를 전달하며 api는 ExceptionResponse에 정의된 필드들로 값을 리턴하게 됩니다.

 

 


 

이러한 흐름으로 예외처리하는 방법에 대해서 공유해보았습니다. 

 

서비스에서의 모든 Exception을 RuntimeException으로 던져 비즈니스 로직에 맞지 않는 값이 request 요청으로 넘어오게 된다면 모든 판단을 Service 로직에서 처리하여 맞으면 통과하고 틀리면 RuntimeException으로 예외를 던져서 비즈니스 로직에 따른 에러처리도 가능합니다!

 

참고! Spring Hateoas 프로젝트를 이용하여 link 정보를 리턴하게 될 때, EntityModel 객체를 Object 객체로 변환하여 리턴하게 됩니다.

이러한 경우에는 link 정보가 원하는 모습으로 보여지지 않게되는데!!

이때는 RestControllerAspect를 제거하고 바로 EntityModel 객체를 response body로 리턴해야 원하는 "_links" : {} json 객체의 모습이 보일 것입니다.

 

유용하게 활용해보셨으면 좋겠습니다!

댓글

Designed by JB FACTORY