스프링 AOP : @Aspect를 실제 프로젝트 적용

    이 포스팅은 인프런 영한님의 강의를 복습하고 정리한 글입니다. 


    @Aspect

    @Aspect를 단 클래스에 어드바이스와 포인트컷을 구현해주고, 이 클래스를 스프링 빈으로 등록해주면 스프링 AOP가 제공해주는 빈 후처리기(Bean PostProcessor)인 AnnotationAwareAutoProxyCreator는 내부적으로 이 @Aspect 스프링 빈을 어드바이저로 만들어 @AspectAdvisorBuilder에 저장해둔다.

    AnnotationAwareAutoProxyCreator는 다른 스프링 빈을 등록할 때, 포인트컷으로 어드바이스의 대상이 되는지 확인한다. 이 때 살펴보는 어드바이저는 스프링 빈으로 등록된 어드바이저, @Aspsect Adviosr Builder 내부의 저장소에 저장된 어드바이저를 대상으로 다른 스프링 빈을 살펴본다. 이 때 다른 스프링 빈이 포인트컷의 적용 대상이 되면, 어드바이저를 추가한 프록시 객체를 만들고 바꿔치기 해서 스프링 빈으로 등록해준다.

     

    이처럼 @Aspect를 활용하면 어드바이저를 추가한 동적 프록시 객체를 만들어 주기 때문에, 원본 코드의 변경 없이 필요한 횡단 관심사를 추가할 수 있다는 장점이 있다. 

     


    @Aspsect를 실제 프로젝트에 적용하기

    @Aspect는 횡단 관심사를 해결하는데 적절하다. 가장 대표적인 AOP 적용 대상은 로그 추적기, @Transactional 같은 어노테이션의 처리다. 이 게시글에서 로그 추적기와 재시도 AOP를 만들어서 적용해보고자 한다.

     


    테스트 코드 준비하기

    리포지토리 코드 → 5번 시도하면 1번은 예외발생

    @Repository
    public class ExamRepository {
    
        // 5번에 1번씩 에러 발생
        private static int seq = 0;
    
        @Trace
        @Retry(maxRetryCount = 10)
        public String save(String itemId) {
            seq ++;
            if (seq % 2 == 0) {
                throw new IllegalStateException("예외 발생");
            }
            return "ok";
        }
    
    }

     

    서비스 코드 → 리포지토리와 의존관계 설정됨.

    @Service
    @RequiredArgsConstructor
    public class ExamService {
    
        private final ExamRepository examRepository;
    
        @Trace
        public void request(String itemId) {
            examRepository.save(itemId);
        }
    }

     

    테스트 코드 작성

    @SpringBootTest
    public class ExamTest {
    
        @Autowired
        ExamService examService;
    
        @Test
        void test() {
            for (int i = 0; i < 5; i++) {
                examService.request("data" + i);
            }
        }
    }
    • 5번 반복해서 examService.request() 메서드를 호출하는 테스트 코드를 작성했다. 

     


    로그 추적기 Aspect 설정하기

    이번 포스팅에서는 메서드에 특정 어노테이션이 달려있으면 포인트컷의 대상이 되도록 설계를 했다. 이의 구현을 위해서 필요한 것은 두 가지다. 

    • 어노테이션 클래스 만들기
    • @Aspect가 있는 로그 추적기 클래스 만들기 

     

    어노테이션 클래스 만들기

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Trace {
    }
    • Retention은 RUNTIME으로 설정해 런타임 시점에 결정되도록 한다
    • @Target은 메서드에만 쓸 것이기 때문에 METHOD로 설정한다. 

     

    @Aspect 클래스 만들기

    @Aspect
    @Slf4j
    @Component
    public class TraceAspect {
    
        @Pointcut("@annotation(hello.aop.exam.annotation.Trace)")
        public void tracePointcut(){}
    
    
        @Before("tracePointcut()")
        public void doLogBefore(JoinPoint joinPoint) {
            log.info("[Trace {} Before]", joinPoint.getSignature().toShortString());
        }
    
        @After("tracePointcut()")
        public void doLogAfter(JoinPoint joinPoint) {
            log.info("[Trace {} After]", joinPoint.getSignature().toShortString());
        }
    
    }
    • @annotation으로 포인트컷 대상을 @Trace로 설정했다. 
    • @Before 어드바이스로 시작할 때, 한번 보여주고, @After 어드바이스로 끝나고 다시 한번 보여줄 수 있도록 했다. 
    • @Component를 이용해 스프링 빈으로 등록해주었다. 스프링 빈으로 등록되어야 스프링 빈 후처리가 인식하고 어드바이저로 만들어준다. 

     

    어노테이션 적용하기 

    @Service
    @RequiredArgsConstructor
    public class ExamService {
    
        private final ExamRepository examRepository;
    
    	// 아래 어노테이션 추가.
        @Trace
        public void request(String itemId) {
            examRepository.save(itemId);
        }
    }

    @Trace 어노테이션이 달리는 메서드는 이제 포인트컷 대상이 되기 때문에 어드바이스가 적용된다. 따라서 이 때, ExamService는 스프링 빈으로 등록될 때, 내부적으로 ExamRepository를 상속받아 만들어진 CGLIB 프록시 객체가 내부적으로 TraceAspect 어드바이스를 가지는 형태로 만들어진다. 

     

    로그 추적기 AOP 적용결과 확인

    • 정상적으로 로그 추적기가 적용된 것이 확인되었음.
    • 5번에 1번씩 예외가 발생하기 때문에 예외가 발생되는 것을 확인할 수 있음. 

     


    재시도 Aspect 적용하기

    위의 테스트 코드를 보면 예외가 발생한다. 예외가 발생했을 때, 몇 번 정도는 재시도를 해주는 것이 도움이 될 것이라 판단하고 재시도 Aspect를 넣기로 했다. 재시도 Aspect는 메서드 어노테이션을 기반으로 AOP를 적용할 것이다. 이를 위해서는 두 가지 수행이 필요하다.

    • @Retry 메서드 어노테이션 만들기
    • @Aspect로 Retry를 적용할 어드바이저 클래스 만들기 

     

    @Retry 어노테이션 만들기

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Retry {
        int maxRetryCount() default 3;
    }
    • Retry 어노테이션을 만든다.
    • 어노테이션에는 최대 재시도 값을 넣어줄 수 있도록 maxRetryCount 변수를 추가했다. 기본값은 3이다.

     

    @Aspect 클래스 만들기

    재시도를 하기 위해서는 다음 로직으로 진행해야한다.

    1. 일단 시도해본다.
    2. 예외가 발생하면, 예외를 잡고 재시도 한다.
    3. 정상 성공이 되면 예외는 먹고 없애면 된다. 마지막까지 성공 되지 않으면, 잡고 있던 예외를 던지면 된다. 
    @Aspect
    @Slf4j
    @Component
    public class RetryAspect {
    
        Exception exceptionHolder;
    
        @Around("@annotation(retry)")
        public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
            log.info("[doRetry] {}, retry = {}", joinPoint.getSignature().toShortString(), retry);
    
            int maxLimit = retry.maxRetryCount();
    
            for (int i = 0; i < maxLimit; i++) {
                try {
                    log.info("[doRetry] {}, retry = {}/{}", joinPoint.getSignature().toShortString(), i, maxLimit);
                    return joinPoint.proceed();
                } catch (Exception e) {
                    //실패시 Exception을 잡아둔다.
                    exceptionHolder = e;
                }
            }
            throw exceptionHolder;
        }
    }
    • Retry retry로 어노테이션을 받았다. 이 때 Retry 타입의 객체를 파라미터에 선언하면서, retry 객체는 패키지 + 클래스명을 가리킨다.
    • 따라서 retry 변수를 @annotation으로 넘겨주면, 자동으로 패키지 + 클래스가 넘어간다.
    • Retry retry는 @Retry 어노테이션에 대한 정보를 가져왔다. 따라서 어노테이션 내부에 있는 정보값을 가져올 수 있다.
    • 성공하면 Return하고, 실패하면 @Retry 어노테이션에 입력한 수만큼 반복한다. 마지막까지 성공하지 못하면, 가지고 있던 예외를 던진다. 

     

    리포지토리에 @Retry 적용하기

    @Repository
    public class ExamRepository {
    
        // 5번에 1번씩 에러 발생
        private static int seq = 0;
    
    	//@Retry 어노테이션 적용
    	@Retry(maxRetryCount = 10)
        @Trace
        public String save(String itemId) {
            seq ++;
            if (seq % 2 == 0) {
                throw new IllegalStateException("예외 발생");
            }
            return "ok";
        }
    
    }
    • 리포지토리의 save 메서드에 @Retry 어노테이션을 적용했다.
    • 이런 이유로 ExamRepository는 RetryAspect 어드바이저의 어드바이스 적용 대상이 되어 프록시 객체로 만들어서 스프링 빈 등록이 된다. 

     

    재시도 AOP 적용결과 확인

    • 이전에는 5번에 1번씩 실패를 했기 때문에 예외가 터지면서 마무리 되었다. 
    • 재시도 AOP를 적용한 이후에는 문제가 발생했을 때, Retry를 통해서 처리해줄 수 있게 되었다. 

    댓글

    Designed by JB FACTORY