관리 메뉴

나구리의 개발공부기록

스프링 AOP - 실전예제, 예제 만들기, 로그 출력 AOP, 재시도 AOP 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

스프링 AOP - 실전예제, 예제 만들기, 로그 출력 AOP, 재시도 AOP

소소한나구리 2024. 11. 15. 09:54

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 예제만들기

1) 예제

  • @Trace 애노테이션으로 로그 출력
  • @Retry 애노테이션으로 예외 발생시 재시도

(1) ExamRepository

  • exam패키지를 생성하여 작성
  • save메서드가 5번 호출되면 1번은 예외가 발생하도록 적용한 저장소
package hello.aop.exam;

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

 

(2) ExamService

  • 비즈니스 로직이 있는 서비스
package hello.aop.exam;

@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

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

 

(3) ExamTest

  • test 하위에 exam패키지 생성 후 작성
  • 5번째 루프가 실행될 때 리포지토리에서 예외가 발생함
package hello.aop.exam;

@Slf4j
@SpringBootTest
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);
        }
    }
}

2. 로그 출력 AOP

1) 로그 출력용 AOP

  • @Trace가 메서드에 붙어있으면 호출 정보가 출력되는 기능

(1) @Trace

  • exam하위에 annotation 패키지 생성 후 작성
  • @Target: METHOD
  • @Retention: RUNTIME
package hello.aop.exam.annotation;

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

 

(2) TraceAspect

  • exam하위에 aop패키지를 생성 후 작성
  • @Before와 @annotation 지시자를 사용하여 포인트컷을 적용하여 @Trace가 붙은 메서드에 어드바이스를 적용함
  • getArgs()로 인수 정보를 조회하고 시그니처와 함께 로그를 출력
package hello.aop.exam.aop;

@Slf4j
@Aspect
public class TraceAspect {

    @Before("@annotation(hello.aop.exam.annotation.Trace)")
    public void before(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();    // 인수 정보 조회
        log.info("[trace] {} args={}", joinPoint.getSignature(), args);
    }
}

 

(3) @Trace 적용

  • ExamService의 request메서드와 ExamRepository의 save 메서드에 @Trace애노테이션을 적용

(4) ExamTest - 애스펙트 스프링빈 적용

  • @Import로 로그를 출력하는 Aspect를 스프링빈으로 등록하면, 4번까지 로그를 정상 출력하는 AOP가 적용되었음
  • 현재는 5번째는 예외가 발생하기때문에 테스트에 오류가 나는것이 정상임
package hello.aop.exam;

@Slf4j
@Import(TraceAspect.class)
@SpringBootTest
class ExamTest {
    
     // 기존 로직 동일
    
}

3. 재시도 AOP

1) 예외 발생 시 재시도하는 AOP

  • @Retry 애노테이션이 있으면 예외가 발생했을 때 다시 시도해서 문제를 복구 

(1) @Retry

  • 애노테이션에 int타입 속성의 기본값을 3으로 설정
  • @Target과 @Retention은 @Trace와 동일
package hello.aop.exam.annotation;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 3;
}

 

(2) RetryAspect

  • @annotation(retry), Retury retry를 사용하여 어드바이스에 애노테이션을 파라미터로 전달하고 retry.value()를 통해서 애노테이션에 지정한 값을 가져올 수 있음
  • 예외가 발생하여 결과가 정상 반환되지 않으면 retry.value()만큼 재시도하고 retry.value()까지 넘어갔음에도 정상 반환되지 않는다면 그때는 exceptionHolder에 저장된 예외를 던지도록 작성

** 참고

  • 참고로 Throwable은 Exception 예외보다 더 상위의 예외이기 때문에 프로그램에서 처리할 수 없는 오류가 포함되어있을 수 있으므로 try-catch로 잡지않고 throws로 던졌음
  • 일반적으로 개발할 때 잡는 예외는 Exception까지만 잡음
package hello.aop.exam.aop;

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

@Slf4j
@Aspect
public class RetryAspect {

    @Around("@annotation(retry)")
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
        log.info("[retry] {}, retry={}", joinPoint.toShortString(), 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) @Retry 적용

  • ExamRepository의 save메서드에 @Retry(value = 4)를 적용하여 재시도 횟수를 기본값3이 아닌 4로 적용(value 생략가능)
  • @Retry같은 재시도 로직을 적용할 때 최대 횟수를 지정하는것이 매우 중요함, 실무에서 이런 조건을 빼먹고 적용하면 셀프 DDos 공격과 같이 정상 처리가 될때까지 요청을 계속 보낼 수 있기 때문에 꼭 최대 횟수를 지정해주어야 함

(4) ExamTest - 애스펙트 적용 및 실행

  • 적용한 애스펙트를 둘다 적용하고 실행하여 로그를 확인해보면 try count=1/4에서 에러가 발생했으나 2번째 재시도에서 정상적으로 테스트가 통과된 것을 확인할 수 있음
@Slf4j
//@Import(TraceAspect.class)
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
class ExamTest {
    // 테스트 로직 동일
}

 

** 참고

  • 스프링이 제공하는 @Transactional은 가장 대표적인 AOP임