✔️ 예제 만들기
유요한 스프링 AOP 만들기
- @Trace : 애노테이션으로 로그 출력
- @Retry : 애노테이션으로 예외 발생시 재시도
ExamRepository
@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";
}
}
ExamService
@Service
@RequiredArgsConstructor
public class ExamService {
private final ExamRepository examRepository;
@Trace
public void request(String itemId) {
examRepository.save(itemId);
}
}
ExamTest
@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);
}
}
}
5번 요청하는 테스트이며, 5번째 요청시 리포지토리에서 예외 발생하여 실패한다.
✔️ 로그 출력 AOP
@Trace 애노테이션을 붙이면 호출 정보(log)가 출력되는 기능을 가진 AOP
@Trace
@Target(ElementType.METHOD) // 메서드타겟
@Retention(RetentionPolicy.RUNTIME) // 애노테이션 유지 정책, 런타임(동적)에 사용 가능
public @interface Trace {
}
TraceAspect
@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);
}
}
다시 말하지만 @Aspect 했다고 스프링 빈에 등록되는 것이 아니다. Import or Bean 수동 등록 or @Component로 등록해야한다.
@Slf4j
@Import(TraceAspect.class)
@SpringBootTest
public class ExamTest {...}
실행 결과
hello.aop.exam.ExamTest : client request i=0
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data0]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
hello.aop.exam.ExamTest : client request i=1
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data1]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
hello.aop.exam.ExamTest : client request i=2
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data2]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
hello.aop.exam.ExamTest : client request i=3
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data3]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
hello.aop.exam.ExamTest : client request i=4
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data4]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
java.lang.IllegalStateException: 예외 발생
✔️ 재시도 AOP
@Retry
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int value() default 3;
}
RetryAspect
@Slf4j
@Aspect
public class RetryAspect {
// 원래는 @annotation(Retry 패키지명)을 다 적었어야하는데 retry로 대체가능하다. 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;
}
}
- 재시도 애스팩트
- @annotation(retry), Retry retry 를 사용해서 어드바이스에 애노테이션을 파라미터로 전달
- retry.value() 를 통해서 애노테이션에 지정한 값을 가져올 수 있음
- 예외 발생하여 결과 정상 반환되지 않으면 retry.value() 만큼 재시도
ExamRepository
@Trace
@Retry(value = 4) // 중요한 점은 횟수 제한이 있어야한다., 파라미터로 값을 넣어 변경 가능 , value 생략 가능
public String save(String itemId) {
seq++;
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
@Slf4j
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
public class ExamTest {...}
실행결과
hello.aop.exam.ExamTest : client request i=0
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data0]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
hello.aop.exam.aop.RetryAspect : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect : [retry] try count=1/4
hello.aop.exam.ExamTest : client request i=1
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data1]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
hello.aop.exam.aop.RetryAspect : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect : [retry] try count=1/4
hello.aop.exam.ExamTest : client request i=2
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data2]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
hello.aop.exam.aop.RetryAspect : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect : [retry] try count=1/4
hello.aop.exam.ExamTest : client request i=3
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data3]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
hello.aop.exam.aop.RetryAspect : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect : [retry] try count=1/4
hello.aop.exam.ExamTest : client request i=4
hello.aop.exam.aop.TraceAspect : [trace] void hello.aop.exam.ExamService.request(String) args=[data4]
hello.aop.exam.aop.TraceAspect : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
hello.aop.exam.aop.RetryAspect : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect : [retry] try count=1/4
hello.aop.exam.aop.RetryAspect : [retry] try count=2/4
실행 로직 이해
재시도 애스펙트는 애노테이션 value 값만큼 재시도
test에서 5번 service를 호출
repository.save에서 seq가 5일때 예외 발생
@Retry가 붙은 save 메서드는 value만큼 재시도
seq가 6이므로 정상적으로 결과 반환
try count는 2/4 로 변경됨 (1 -> 2번째 시도)
ps.
AOP에 대해서 배우고 나니 @Transactional이 이렇게 만들어진거구나 싶다.
항상 애노테이션을 어떻게 작동하게 하는지도 몰랐는데 애스펙트를 사용하면 된다는 것을 알게되었다.
🔖 학습내용 출처
'Back-End > Spring Advance & Boot' 카테고리의 다른 글
스프링 부트 - 웹 서버와 서블릿 컨테이너 (2) | 2024.11.15 |
---|---|
스프링 고급편 - 실무 주의사항 (내부호출, 기술과 한계) (1) | 2024.11.13 |
스프링 고급편 - 포인트컷 (0) | 2024.11.11 |
스프링 고급편 - 스프링 AOP 구현 (0) | 2024.10.31 |
스프링 고급편 - 스프링 AOP 개념 (0) | 2024.10.04 |