Question
- 예외 발생시 재시도를 하는 로직을 AOP를 통해 구현한다면?
- AOP 사용시 같은 클래스 내부의 메서드 호출시 잘 동작하지 않는 이유는?
- 위 문제를 해결할 방법은?
- JDK 동적 프록시와 CGLIB 방식의 장단점은?
- 스프링이 기본적으로 채택한 프록시 방식은?
예외 발생시 재시도를 하는 로직을 AOP를 통해 구현한다면?
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int value() default 3;
}
@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;
}
}
- @Retry 어노테이션 생성하고, 재시도 횟수를 value로 지정
- AOP에서 @annotation으로 조인포인트 맵핑
- 파라미터로 retry를 받아서, retry에 지정된 value를 가져와 그 횟수만큼 재시도
- @Around 안에서 joinPoint.proceed()는 여러번 시도 가능
AOP 사용시 내부 호출이 동작하지 않는 문제
- 같은 클래스 내의 함수에서 같은 클래스 내의 함수를 호출하면 AOP가 동작하지 않음
- 예를 들어서, this.method()와 같은 경우
- 그 이유는, AOP는 프록시를 거쳐서 호출이 되야 하는데, 내부를 호출할 때는 프록시가 동작하지 않기 때문
- 위 그림에서 external()과 internal()은 같은 클래스 내의 함수
- 클라이언트가 external()을 호출할 때는 프록시가 만들어지며 프록시의 external이 진짜 실제의 external()을 호출
- 진짜 실제의 external()은 internal()을 호출할 때 프록시를 거치지 않고 진짜 자신의 internal()을 호출
- 따라서, 프록시를 거치지 않기 때문에 AOP가 동작하지 않음.
- AspectJ를 사용하면 AspectJ는 프록시 방식을 사용하지 않고 실제 바이트 코드를 조작해 함수 앞뒤로 어드바이스를 넣어주기 때문에 이러한 문제가 발생하지는 않음
프록시 내부 호출 문제 해결 방법
- 자기 자신을 의존관계 주입을 받는 방법(Setter 방식으로)이 있는데 좋은 방법은 아님
- ObjectProvider(Provider)를 사용하여 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하 는 시점으로 지연 하는 방법
- 구조를 변경하여 내부를 호출해야 하는 함수를 별도 클래스로 만들어서 사용
- 일반적으로 3번째 방법이 그나마 나은 경우지만 AOP는 일반적으로 부가기능의 용도이므로 꼭 이렇게 사용해야 할 케이스가 흔하진 않음
프록시 기술과 한계 - 타입 캐스팅
- JDK 동적 프록시 방식은 인터페이스가 있는 경우에만 가능함
- CGLIB 방식은 구체클래스에도 가능하지만 여러 문제가 있음
- 대상 클래스에 기본 생성자 필수
- 생성자 2번 호출 문제
- final 키워드 클래스, 메서드 사용 불가
- 하지만 스프링4.0부터는 objenesis라는 특별한 라이브러리를 사용하여 기본 생성자 필수, 생성자 2번 호출 문제를 CGLIB에서 해결함
- final 키워드는 사실상 잘 사용하지 않으므로 스프링 부트 2.0 부터는 CGLIB을 기본으로 사용하는 방식을 채택