스프링 AOP : @Aspect를 실제 프로젝트 적용
- Spring/Spring
- 2022. 2. 6.
이 포스팅은 인프런 영한님의 강의를 복습하고 정리한 글입니다.
@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 클래스 만들기
재시도를 하기 위해서는 다음 로직으로 진행해야한다.
- 일단 시도해본다.
- 예외가 발생하면, 예외를 잡고 재시도 한다.
- 정상 성공이 되면 예외는 먹고 없애면 된다. 마지막까지 성공 되지 않으면, 잡고 있던 예외를 던지면 된다.
@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를 통해서 처리해줄 수 있게 되었다.
'Spring > Spring' 카테고리의 다른 글
스프링 AOP : 프록시 기술과 한계 (0) | 2022.02.06 |
---|---|
스프링 AOP : 프록시 내부 참조 → AOP 적용 안되는 문제 (0) | 2022.02.06 |
Spring AOP : 포인트컷 지시자 (0) | 2022.02.04 |
Spring AOP : @Aspect를 이용한 프록시 객체 실습 (0) | 2022.02.02 |
스프링 AOP : 스프링이 제공하는 빈 후처리기, AutoProxyCreator (1) | 2022.01.29 |