프록시 생성 방식
public class MainAspect {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
// calculator 빈의 실제 타입은 Calculator를 상속한 프록시 타입이므로 RecCalculator로 받으면 타입 변환이 불가하여 익셉션 발생
Calculator cal = ctx.getBean(Calculator.class);
long fiveFact = cal.factorial(5);
System.out.println("cal.factorial(5) = " + fiveFact);
System.out.println(cal.getClass().getName());
ctx.close();
}
}
실행코드에서 calculator 빈을 받아오는 소스를
이렇게 바꾸면 어떻게 될까?
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
RecCalculator 타입을 리턴하니까 아무 문제가 없지 않을까?
public Calculator calculator() {
return new RecCalculator();
}
정답은 ❌ 이다
[ERROR] [system.err]
Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'calculator' is expected to be of type 'chap07.RecCalculator' but was actually of type 'jdk.proxy2.$Proxy18'
스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체(RecCalculator)가 인터페이스(Calculator)를 상속하면 인터페이스를 이용해서 프록시를 생성한다.
따라서 빈의 실제 타입이 RecCalculator라고 하더라도 "calculator" 이름에 해당하는 빈 객체의 타입은 Calculator 인터페이스를 상속받은 프록시 타입이 된다.
즉 에러메시지를 다시 보면 스프링이 런타임에 생성한 프록시 객체인 $Proxy18은 Calculator 인터페이스를 상속받았는데 getBean() 메서드에 사용한 타입은 RecCalculator라 다르다고 익셉션이 발생한 것 !
빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용하여 프록시를 생성하고 싶다면
package config;
import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// @Aspect 어노테이션을 붙인 클래스를 공통 기능으로 적용하기 위해 붙여줌
// 이 어노테이션을 추가하면 스프링은 @Aspect 어노테이션이 붙은 빈 객체를 찾아서 빈 객체의 @Pointcut 설정과 @Around 설정 사용
// 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면
// proxyTargetClass 속성을 true로 지정하면 됨
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx {
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
// AOP 적용 시 RecCalculator가 상속받은 Calculator 인터페이스를 이용해서 프록시 생성
public Calculator calculator() {
return new RecCalculator();
}
}
@EnableAspectJAutoProxy 어노테이션에 (proxyTargetClass = true) 옵션을 넣어주면 된다. 속성을 true로 지정하면 인터페이스가 아닌 자바 클래스를 상속받아 프록시를 생성한다.
🙋♀️도대체 순서가 어떻게 되나요?
💡 1.런타임에 프록시 객체가 생성 -> 2.설정파일을 읽어 aspect를 구현한 클래스 탐색 -> 3.프록시 대상 객체가 호출될 때 weaving이 일어나 공통 기능이 적용 -> 4.aspect 클래스에 proceed()를 통해 비즈니스 메서드를 호출하고 결과를 리턴
너무 나만 이해한 답변같지만 다음에 말 정리가 잘 됐을 때 다시 수정해야겠다 ..
execution 명시자 표현식
execution 명시자는 Advice를 적용할 메서드를 지정할 때 사용
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))
Advice 적용 순서
한 Product에 여러 advice 적용 가능하며, 어떤 aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있다
적용순서가 중요하다면 @Order 어노테이션을 통해 적용 순서를 지정해주면 됨
@Aspect
@Order(1)
public class ExeTimeAspect {
...
}
@Aspect
@Order(2)
public class CacheAspect {
...
}
@Around의 Pointcut 설정과 @Pointcut 재사용
@Pointcut 어노테이션이 아닌 @Around 어노테이션에 execution 명시자를 직접 지정할 수도 있다.
@Aspect
public class CacheAspect {
@Around("execution(public * chap07..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Trowable {
...
}
}
만약 같은 Pointcut을 여러 Advice가 함께 사용한다면 공통 Pointcut을 재사용할 수도 있다.
@Aspect
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Trowable {
...
}
}
이 코드에서 @Around는 publicTarget() 메서드에 설정한 Pointcut을 사용한다.
공통으로 사용하는 Pointcut이 있다면 별도 클래스에 포인트 컷을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성하면 Pointcut 관리가 편해진다.
public class CommonPointcut {
@Pointcut("execution(public * chap07..*(..))"){
public void commonTarget(){
}
}
@Aspect
public class CacheAspect{
@Around(CommonPointcut.commonTarget())
}
위처럼 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 @Around 어노테이션에서 사용하면 된다.
메섣에 -> 오타가 있네요.