본문 바로가기
스프링

[스프링] AOP 와 @RestControllerAdvice

by kdohyeon (김대니) 2023. 2. 22.
반응형

지금 회사에 들어오면서 처음 봤던 코드 중 하나가 @RestControllerAdvice 였습니다. 이전 회사에서는 예외 처리를 가장 앞단의 try-catch 로 묶어서 했었는데, 여기서는 Advice 를 적극적으로 활용하고 있어 이에 대해 정리해두고자 합니다.


스프링 AOP

AOP 는 Aspect-Oriented Programming 의 약자로 관점 지향 프로그래밍이다. 처음에 관점 지향 프로그래밍? 이라고 하길래 객체 지향 프로그래밍, 절차 지향 프로그래밍 등 뭔 그놈의 지향이 많은지 싶었다. 평소에 잘 작성하지 않는 메커니즘으로 구현하는 방식이다 보니 잘 와닿지 않았고, 이해하는데도 시간이 좀 걸렸었다.

횡단 관심사의 분리

AOP 을 통해 이루고자 하는 것은 관심사의 분리이다. 위 그림처럼 "사용자 정보 관리", "주문 정보 관리", "배송 정보 관리" 등이 핵심 비즈니스 로직으로 구현되고, 공통적으로 "로깅", "보안", "트랜잭션"과 같은 개념을 활용할 수 있다. 만약 사용자를 조회하는 로직의 시작과 끝에 로그를 심어야 하는 문제가 생겼다고 하면, 아래처럼 간단하게 추가할 수 있다.

public class UserService {
    public UserDto findBy(String id) {
        log.info("start finding user by id {}", id);
        
        // find user
        
        log.info("finished finding user - name={}, user.getName());
    }
}

그런데 만약 모든 메소드의 시작과 끝에 관련 로그를 심어야 한다면 어떻게 해야 할까? 복사 붙여넣기를 할 수 있겠지만 중복 코드가 엄청나게 발생하며 로그의 형식이 변경되면 모든 코드를 찾아가며 변경해주어야 한다 (IDE 의 도움을 받을 수 있겠지만)

이를 해결하기 위한 방법으로 AOP 를 활용할 수 있는데, 특정 메소드의 시작 또는 끝에 원하는 액션을 취하게 할 수 있고, 메소드가 성공했을 때만, 또는 실패했을 때만 실행하게 설정을 할 수도 있다. 스프링에서 이미 제공하고 있는 AOP 기능으로는 @Transactional, @Cachable 등이 존재한다.

AOP  개념

AOP 에는 몇 가지 개념이 존재하는데, Aspect, Advice, Join Point 등이 존재한다.

  • Aspect. AOP 에서 A 에 해당하는 부분이며 하나의 단위가 되는 횡단 관심사를 의미한다. 예를 들면, "로그를 출력", "예외를 처리", "트랜잭션을 관리"한다 등의 Aspect 가 존재할 수 있다.
  • Join Point. 횡단 관심사가 실행될 지점이나 시점을 의미한다. 스프링에서 제공되는 모든 메소드라고 봐도 된다.
  • Advice. Join Point 에서 실행될 코드로 Aspect 에 대해 구현 및 처리를 하는 부분이다.
  • Point Cut. 여러 Join Point 중 Advice 를 적용할 곳을 선별하기 위한 표현식이다. 
  • Weaving. Join Point 에 Advice 를 적용한다.
  • Target. 적용될 대상을 의미한다.

아래 코드를 살펴보면:

  • @Before 어노테이션: Advice 의 동작 시점으로 메소드 시작 전에 실행
  • "execution(* *..*ServiceImpl.*(..))*": 클래스명이 ServiceImpl 으로 끝나는 모든 클래스의 메소드에 해당
@Aspect // Aspect 로 인식할 수 있도록 
@Slf4j 
@Component // IoC 컨테이너에서 관리
public class MethodStartLoggingAspect {
    @Before("execution(* *..*ServiceImpl.*(..))*)
    public void startLogging(JoinPoint jp) {
        log.info("start method... {}", jp.getSigniture());
    }
}
 

 

@RestControllerAdvice

@RestControllerAdvice 어노테이션을 통해 호출되는 요청에 대한 예외를 한 곳에서 관리할 수 있도록 한다. 아래처럼 별도의 클래스를 만들고 @RestControllerAdvice 어노테이션을 부여한다. @ResponseStatus 어노테이션을 통해 INTERNAL_SERVER_ERROR (5xx) 에러를 반환하게 한다. @ExceptionHandler 는 요청을 처리하면서 발생하는 특정 Exception 유형을 처리할 수 있게 한다. 아래 예시에서는 Exception.class 에 해당하는, 즉 모든 예외를 다 잡겠다는 의미이다.

@Slf4j
@RestControllerAdvice
public class BlogControllerAdvice {
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    protected ResultResponse<Void> handleUnknownException(Exception e) {
        String errorId = UUID.randomUUID().toString();
        log.error("[{}] Unknown Exception. message={}", errorId, e.getMessage(), e);
        return ResultResponse.fail(String.format("[%s] 알 수 없는 오류가 발생하였습니다. msg: %s", errorId, e.getMessage()));
    }
}

 @RestControllerAdvice 를 사용하지 않으면?

이 코드를 사용하지 않고 비슷한 효과를 내려면 아래처럼 구현하면 된다. 모든 컨트롤러에 try-catch 문을 복사 붙여넣기 형식으로 코드를 넣어두면 해결할 수 있다.

@GetMapping("/api/v1/blogs")
    public ResultResponse<BlogDto> searchBlogs(
            @Valid SearchBlogRequestBody requestBody,
            @RequestParam(required = false, defaultValue = "1") int page,
            @RequestParam(required = false, defaultValue = "10") int size
) {
    try {
        ...
    } catch (Exception e) {
        String errorId = UUID.randomUUID().toString();
        log.error("[{}] Unknown Exception. message={}", errorId, e.getMessage(), e);
        return ResultResponse.fail(String.format("[%s] 알 수 없는 오류가 발생하였습니다. msg: %s", errorId, e.getMessage()));
    }
}

 

반응형

댓글