지금 회사에 들어오면서 처음 봤던 코드 중 하나가 @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()));
}
}
'스프링' 카테고리의 다른 글
[스프링] @ConstructorProperties 활용하기 (feat. Jackson 라이브러리) (0) | 2023.03.05 |
---|---|
[스프링] @PostMapping 의 속성 알아보기 (headers, produces) (0) | 2023.02.27 |
[Gradle] Kotlin DSL 과 buildSrc 를 통한 버전 관리 (0) | 2023.02.20 |
[Gradle] Build Lifecycle (0) | 2023.02.16 |
[스프링] @CircuitBreaker 적용하기 (0) | 2023.02.16 |
댓글