AspectJ로 final method에도 위빙하기

소스 코드는 이전과 동일합니다. 그 상태에서 프록젝트에 AspectJ Nature를 추가해줍니다.

사용자 삽입 이미지
그런 다음에 프로젝트 클린을 하여 기존의 클래스파일을 비우고 다시 컴파일하게 합니다.(Alt + P, N, 엔터) 지금, 저는 아무런 .aj 파일도 만들지 않았습니다. 그냥 이전에 만들어둔 Spring @AOP를 사용하고 있을 뿐입니다.

@Aspect
public class FinalHelloAspect {

    @Before(“execution(* org.opensprout.sandbox.proxy.withfinal.FinalHello.*(..))”)
    public void withFinalAdvice() {
        System.out.println(“before advice from FinalHelloAspect”);
    }

}

이 녀석이죠. AspecJ 프로젝트가 됐기 때문에, 빌드타임에 저 애스팩트의 포인트컷에 해당하는 클래스 파일을 조작해서 “새로운 .class 파일”을 생성하게 될 겁니다.

사용자 삽입 이미지
컴파일이 끝난 후 AspectJ IDE가 발동한 모습이니다. 포인트컷 마다 저런 화살표가 표시됩니다. 보시면 final 메소드에도 화살표가 표시되어 있습니다. 해당 코드와 Cross References 뷰를 동기화 시켜두면, 해당 지점에 적용될 어드바이스까지 표시됩니다.

사용자 삽입 이미지
놀랍습니다. 안 그러세요? @Aspect 캬오~ 하긴, 그리 놀랍지도… @Aspect 애노테이션이 aspectj 프로젝트에 속한 애노테이션이니까,..

사용자 삽입 이미지
이 결과를 보면 더이상 프록시를 사용하고 있는것이 아니기 때문에, Advised 라는 인터페이스는 사용할 수 없다는 것을 볼 수 있습니다. ClassCastException이 보이시죠?

흠냐;;

회사 청소해야되서 이만.. 황급히 마무리 합니다.

CGLIB 프록시 제약 사항 테스트.

@Component
public class FinalHello implements Hello {

    public String hi() {
        return “hi”;
    }
    
    public final void finalHi(){
        System.out.println(“안녕”);
    }
    
}

위 클래스에 모든 메소드 호출을 포인트컷으로 간단한 Adivce를 적용하여 CGLIB 프록시를 생성했을 때, 다음과 같은 결과를 확인할 수 있었습니다.

1. final 메소드를 호출할 경우 Advice를 적용하지 않음.
2. final이 아닌 메소드에는 Advice가 적용됨.

이런 간단한 테스트를 해보기 전에는 막연히 final 메소드가 있으면 CGLIB 프록시를 못 만드는 건가 싶었는데, 그게 아니었습니다.

한 가지 더 확인해봤습니다. 기본 생성자(인자가 없는 생성자)가 반드시 있어야 한다고 본것 같아서, 그걸 확인해봤습니다. 위의 경우에는 기본 생성자가 있는 상태로 테스트를 했으니까 기본 생성자 없이 테스트를 한 번 더 해보면 되겠죠.

@Component
public class FinalHello implements Hello {
   
    String hi;
   
    public FinalHello(String hi) {
        this.hi = hi;
    }

    public String hi() {
        return hi;
    }
   
    public final void finalHi(){
        System.out.println(“안녕”);
    }
   
}

그리고 위에서 실행했던 테스트 코드와 애스팩트를 그대로 사용해봤습니다.

java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:203)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109)
    at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:255)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:93)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.invokeTestMethod(SpringJUnit4ClassRunner.java:130)
    at org.junit.internal.runners.JUnit4ClassRunner.runMethods(JUnit4ClassRunner.java:51)
    at org.junit.internal.runners.JUnit4ClassRunner$1.run(JUnit4ClassRunner.java:44)
    at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:27)
    at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:37)
    at org.junit.internal.runners.JUnit4ClassRunner.run(JUnit4ClassRunner.java:42)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:38)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘finalHello’ defined in file [C:\workspace\osaf\target\test-classes\org\opensprout\sandbox\proxy\withfinal\FinalHello.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.opensprout.sandbox.proxy.withfinal.FinalHello]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:883)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:839)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:440)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:409)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:380)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:264)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:221)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:261)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:429)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:729)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:381)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:84)
    at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:42)
    at org.springframework.test.context.TestContext.loadApplicationContext(TestContext.java:173)
    at org.springframework.test.context.TestContext.getApplicationContext(TestContext.java:199)
    … 16 more
Caused by: org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.opensprout.sandbox.proxy.withfinal.FinalHello]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:58)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:877)
    … 33 more
Caused by: java.lang.NoSuchMethodException: org.opensprout.sandbox.proxy.withfinal.FinalHello.<init>()
    at java.lang.Class.getConstructor0(Class.java:2706)
    at java.lang.Class.getDeclaredConstructor(Class.java:1985)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:54)
    … 34 more

맞네요. 두 번쨰 제약 사항까지 살펴봤습니다.

CGLIB 프록시가 JDK 프록시에 비해 성능도 좋고, concrete 클래스의 프록시도 만들어 주기 때문에, 좋긴 한데, 위의 두 개의 제약 사항(final 메소드에는 어드바이스 적용 불가(상속을 못하니까.), 기본 생성자 필요.)에 주의 하면서 사용해야겠습니다.

만약에 이런 경우엔 어떻게 해야 할까요.

1. 구현하는 인터페이스 없음 ==> JDK 프록시 사용불가
2. 기본 생성자 없음 ==> CGLIB 사용불가
3. 어드바이스 적용하고자 하는 메소드가 fianl ==> 역시 CGLIB 사용불가
4. 하지만 AOP 하고파. ==> Spring AOP로는 불가능.

결론(지금은 머리로만 생각한 겁니다. 검증은 조금 뒤에..)
==> Spring AOP + AspectJ 연동해서, 빌드 타임에 aspectj-waever를 사용하던가, 클래스 로딩 타임에 loadtime-weaving을 하면 될 것입니다. AspectJ를 사용한 위빙은 프록시를 만드는게 아니라, 바이트코드랑 .aj 파일을 조작해서 타겟 클래스에 대한 .class파일을 다시 생성하고 그 코드를 사용하는 것이기 때문에 런타임시에 부하도 없고 위와 같은 Spring AOP 제약에서 벗어날 수 있으리라 봅니다.