[Spring] 특정상황에 스프링 AOP 적용하기

Spring AOP를 이용하여 특정상황에 AOP를 적용할 수 있습니다.

이번 포스팅에서 설명할 내용은 다음과 같습니다.

----------------------------------------------------------

  1. 직접 만든 @Trace 어노테이션이 붙은 메소드에 AOP 적용하기
  2. 예외가 발생했을 경우 재시도를 하는 AOP 적용하기
  3. 메소드의 실행시간이 일정 시간을 초과했을 경우 AOP 적용하기

@Trace 어노테이션이 붙은 메소드에 AOP 적용

먼저 직접 만든 @Trace 어노테이션이 붙은 메소드에 AOP 적용하는 방법을 알아 보겠습니다.

 

1. @Trace 어노테이션을 생성합니다.

@Trace 어노테이션
package hello.aop.exam.annotation;
...

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

 

2. @Trace 어노테이션이 붙은 메서드에 로그를 출력하는 Advisor를 만듭니다.

TraceAspect
package hello.aop.exam.aop;
...

@Slf4j
@Aspect
public class TraceAspect {

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

 

 

3. AOP를 적용 Repository, Service를 생성합니다.

ExamRepository

데이터를 저장시 (save) seq 값(ID)이 5번 째가 될 때마다 예외가 발생합니다.

save() 메서드 실행 시 로그를 적용하기 위해 @Trace 어노테이션을 붙여줍니다.

package hello.aop.exam;
...

@Repository
public class ExamRepository {
    private static int seq = 0;

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

 

ExamService

클라이언트의 요청을 받아 요청 값을 ExamRepository에 저장합니다.

request() 메서드 실행 시 로그를 적용하기 위해 @Trace 어노테이션을 붙여줍니다.

package hello.aop.exam;
...

@Service
@RequiredArgsConstructor
public class ExamService {
    private final ExamRepository examRepository;

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

 

4. 이제 테스트 코드로 @Trace 어노테이션이 붙은 메서드에 로그 출력 기능이 잘 적용되는지 확인해봅시다.

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

    @Autowired
    ExamService examService;

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

 

로그를 확인해 보면 TraceAspect가 잘 적용 되어, @Trace 어노테이션이 붙은 메서드의 로그 정보가 찍히는 것을 볼 수 있습니다.

그런데 여기서 5번 째 요청에 예외가 발생합니다.

바로 ExamRepository에서 데이터를 저장시 (save) seq 값(ID)이 5번 째가 될 때마다 예외가 발생하도록 설정했기 때문인데요.

재시도 AOP를 적용하여 이 부분을 해결 해 봅시다.


예외가 발생했을 경우 재시도 AOP 적용

재시도 AOP를 적용하는 경우는 예를 들어서

"서버와 서버가 통신을 할 때, 어쩌다 한번씩 오류가 발생해서 다시 요청하면 성공할 경우"에 사용할 수 있습니다.

 

이제 재시도를 적용할 @Retry 어노테이션을 만들어서 적용 해 보겠습니다.

 

1. @Retry 어노테이션 생성합니다.

@Retry 어노테이션

재시도 AOP를 적용할 때는 주의할 점이 있습니다.

바로 재시도 횟수를 꼭 제한 해줘야 합니다. (여기서는 재시도 횟수를 기본 값 3으로 설정하였습니다.)

package hello.aop.exam.annotation;
...

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    // 재시도 AOP를 설정할 때는 꼭 재시도 횟수를 제한해야 한다.
    int value() default  3; // 기본값 3
}

 

2. @Retry어노테이션이 붙은 메서드에 예외가 발생했을 경우 재시도하는 Advisor를 만듭니다.

RetryAspect
  • @annotation(retry) , Retry retry 를 사용해서 어드바이스에 애노테이션을 파라미터로 전달
  • retry.value() 를 통해서 애노테이션에 지정한 값을 가져온다. (@Retry의 기본 값이 3이므로 maxRetry = 3)
  • 지정한 재시도 횟수 동안 재시도를 반복하며, 지정한 횟수에 도달 하기전에 예외가 발생하지 않으면 proceed()로 타겟을 호출
  • 지정한 재시도 횟수까지 예외가 발생한다면 발생한 예외를 출력
package hello.aop.exam.aop;
...

@Slf4j
@Aspect
public class RetryAspect {

    @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;
    }
}

 

3. 예외가 터지는 ExamRespository의 save() 메서드에 재시도를 위해 @Retry 어노테이션을 붙여줍니다.

// 위 ExamRepository 코드와 같음
    @Trace
    @Retry
    public String save(String itemId) {
		...
    }

 

4. 이제 테스트 코드로 예외 발생 시 @Retry 어노테이션이 붙은 메서드에 재시도 기능이 잘 작동하는지 확인해봅시다.

package hello.aop.exam;
...

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

    @Autowired
    ExamService examService;

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

 

4번 째 요청 까지 문제 없이 잘 작동하다가 5번 째 요청에서 예외가 발생하여 재시도 기능이 작동해 2번의 재시도 만에 정상 작동한 것을 확인할 수 있습니다.


특정시간 이상 실행 시 시간초과 알림 AOP 적용

이번에는 메서드의 실행 시간이 일정 시간을 초과했을 경우, 기준 실행 시간을 초과했다고 로그를 찍어주는 시간초과 AOP를 적용해보겠습니다.

 

1. @RunningTimeCheck 어노테이션 생성합니다.

기본값을 1000으로 세팅해, 1000초 안으로 실행되도록 하였습니다.

@RunningTimeCheck
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunningTimeCheck {
    int value() default 1000; // 기본 10초
}

 

 

2. @RunningTimeCheck 어노테이션이 붙은 메서드에서 시간초과가 발생했을 경우 알림을 보내는 Advisor를 만듭니다.

  • @annotation(timeCheck) , RunningTimeCheck timeCheck를 사용해서 어드바이스에 애노테이션을 파라미터로 전달
  • retry.value() 를 통해서 애노테이션에 지정한 값을 가져온다. (@RunningTimeCheck 의 기본 값이 1000이므로 millis = 1000)
  • 편리한 시간측정을 위해 스프링의 StopWatch 클래스를 사용하였습니다.
  • 실행 시간이 기준 시간 (1000)을 넘지 않는다면 [정상 실행] 로그 출력
  • 실행 시간이 기준 시간 (1000)을 넘는 다면 [!경고] 로그 출력
TimeCheckAspect
package hello.aop.exam.aop;
...

@Slf4j
@Aspect
public class TimeCheckAspect {

    @Around("@annotation(timeCheck)")
    public void checkTimer(ProceedingJoinPoint joinPoint, RunningTimeCheck timeCheck) throws Throwable {

        int mills = timeCheck.value(); // 기본 설정 1초

        StopWatch stopWatch = new StopWatch(); // 스프링의 StopWatch 클래스 사용

        stopWatch.start();
        joinPoint.proceed();
        stopWatch.stop();

        long runningTime = stopWatch.getTotalTimeMillis();

        String methodName = joinPoint.getSignature().getName();

        if (runningTime <= mills) {
            log.info("[정상 실행] method = {}, 실행시간 = {} ms", methodName, runningTime);
        } else {
            log.error("[!경고] [기준 실행 시간을 초과하였습니다] method = {}, 실행시간 = {} ms", methodName, runningTime);
        }
    }
}

 

3. [!경고] 알림을 받을 수 있도록 ExamRepository의 save() 메서드에 @RunningTimeCheck 어노테이션을 붙이고 실행시간을 3초가 걸리도록 세팅합니다.

ExamRepository
@Repository
public class ExamRepository {
    private static int seq = 0;

	@RunningTimeCheck
    public String save(String itemId) {
        sleep(3000);
        return "ok";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

4. 이제 테스트 코드로 예외 발생 시 @Retry 어노테이션이 붙은 메서드에 재시도 기능이 잘 작동하는지 확인해봅시다.

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

    @Test
    void test() {
        log.info("--- client request --- ");
        examService.request("data");
    }
}

examService는 클라이언트의 요청을 받아 요청값을 ExamRepository의 save()로 값을 저장하는데

save()의 실행시간이 3초기 때문에 기준 시간 1초(1000)를 초과해 다음과 같이 경고 로그를 출력하는 것을 볼 수 있습니다.


참고자료 : 김영한의 스프링 핵심 원리 - 고급편