0. 예제 만들기

에러가 발생할 때 3번 정도 재호출하는 AOP 만들기

예제 코드

ExamRepository

package hello.aop.exam;

import org.springframework.stereotype.Repository;

@Repository
public class ExamRepository {

    private static int seq = 0;

    /**
     * 5번에 1번 실패하는 요청
     */
    public String save(String itemId){
        seq++;
        if(seq % 5 == 0){
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}
  • 5번 당 1번 에러가 나도록 구성

ExamService

package hello.aop.exam;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

    public void request(String itemId){
        examRepository.save(itemId);
    }
}

ExamTest (test)

package hello.aop.exam;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class ExamTest {

    @Autowired
    ExamService examService;

    @Test
    void test(){
        for(int i = 0; i < 5; i++){
            log.info("client request i={}", i);
            examService.request("data" + i);
        }
    }
}

1. 로그 출력 AOP

로그를 출력해주는 @Trace 와 TraceAspect 를 만들어보자.

@Trace

package hello.aop.exam.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}

TraceAspect

package hello.aop.exam.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Slf4j
@Aspect
public class TraceAspect {

    @Before("@annotation(hello.aop.exam.annotation.Trace)")
    public void doTrace(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        log.info("[trace] {} args={}", joinPoint.getSignature(), args);
    }
}

@annotation(hello.aop.exam.annotation.Trace) 포인트컷을 사용해서 @Trace 가 붙은 메서드에 어드바이스를 적용한다.

ExamService, ExamRepository

서비스 및 리파지토리 메서드에 @Trace 를 붙여준다.

ExamTest - 추가

package hello.aop.exam;

import hello.aop.exam.annotation.Trace;
import hello.aop.exam.aop.TraceAspect;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@SpringBootTest
@Import(TraceAspect.class)
public class ExamTest {

    @Autowired
    ExamService examService;

    @Test
    void test(){
        for(int i = 0; i < 5; i++){
            log.info("client request i={}", i);
            examService.request("data" + i);
        }
    }
}

@Import(TraceAspect.class) 로 빈으로 추가해준다.

실행결과는 아래와 같다.

[trace] void hello.aop.exam.ExamService.request(String) args=[data0]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
[trace] void hello.aop.exam.ExamService.request(String) args=[data1]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
...

2. 재시도 AOP

@Retry

package hello.aop.exam.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {

    int value() default 3;
}

  • 기본 재시도 횟수는 3 이다.

RetryAspect

package hello.aop.exam.aop;

import hello.aop.exam.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class RetryAspect {

    //매개 변수의 retry 에서 타입정보를 읽어서 `("@annotation(retry)")` 에 넣는다.
    @Around("@annotation(retry)")
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {

        log.info("[retry] {} retry={}", joinPoint.getSignature(), retry);

        int maxRetry = retry.value();
        Exception exceptionHolder = null;

        for(int retryCount = 1; retryCount <= maxRetry; retryCount++){
            try {
                log.info("[retry] try count={}/{}", retryCount, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e){
                exceptionHolder = e;
            }
        }
        throw exceptionHolder;
    }
}
  • maxRetry: 최대 재시도 횟수
  • exceptionHolder : 마지막 재시도 때의 Exception
  • for 반복문을 통해 계속 joinPoint.proceed(); 을 반복

ExamRepository - @Retry 추가

package hello.aop.exam;

import hello.aop.exam.annotation.Retry;
import hello.aop.exam.annotation.Trace;
import org.springframework.stereotype.Repository;

@Repository
public class ExamRepository {

    private static int seq = 0;

    /**
     * 5번에 1번 실패하는 요청
     */
    @Trace
    @Retry(4)
    public String save(String itemId){
        seq++;
        if(seq % 5 == 0){
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}

@Retry(4) 와 같이 value 값을 지정할 수 있다. 기본 횟수는 3 이다.

ExamTest - 추가

@SpringBootTest
@Import({TraceAspect.class, RetryAspect.class})
public class ExamTest {
}

@Import({TraceAspect.class, RetryAspect.class}) 를 스프링 빈으로 추가하자.

실행결과

...
[retry] try count=1/5
[retry] try count=2/5

실행 결과를 보면 5번째 문제가 발생했을 때 재시도 덕분에 문제가 복구되고, 정상 응답되는 것을 확인할 수 있다.

댓글남기기