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 제약에서 벗어날 수 있으리라 봅니다.

@Configurable + 톰캣

테스트 코드는 다음과 같습니다.

public class MemberTestServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        Member member = new Member();
        if(member.getMemberRepository() == null)
            System.out.println(“Opps Repository Null”);
        if(member.getMemberRepository().getSessionFactory() == null)
            System.out.println(“Opps SessionFactory Null”);
        System.out.println(“Good!!!”);
    }

}

간단하죠. 뷰에 디스패칭을 하지도 않았습니다. 그냥 콘솔에 Good!!만 출력하도록 했습니다. 그 이외의 경우(Null)에는 화면에 뭐가 Null인지 출력하도록 했죠. 그리고 이 녀석을 web.xml에 등록했습니다.

    <servlet>
        <servlet-name>memberTest</servlet-name>
        <servlet-class>web.MemberTestServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>memberTest</servlet-name>
        <url-pattern>/memberTest.do</url-pattern>
    </servlet-mapping>

그리고 브라우저에서 /memberTest.do 를 호출하고 콘솔 창을 봤습니다.

사용자 삽입 이미지
사용자 삽입 이미지
결론 : @Configurable은 웹 서버에서도 잘 동작 합니다.

@Configurable + @Entity

참조 : Spring: Component Scan + Load Time Weaver (LTW)

아침에 올라온 댓글을 보고 확인해봤습니다.

질문은 @Configurable과 JPA 그리고 Jetty를 사용했을 때, @Configurable이 동작하지 않아서 도메인 객체가 가지고 있는 레퍼런스 타입의 객체들이 세팅되지 않고 null 인 상태라는 제보였습니다.

예상으로는 웹 서버를 동작 시키실 때, -javaagent 옵션을 주지 않으신 게 아닌가 싶습니다.

사용자 삽입 이미지이클립스에서 톰캣을 사용하는 이런 화면에서 가운데 보이는 Open lunch configuration에서 옵션을 줄 수 있습니다.

사용자 삽입 이미지일단 서버에서 테스트 하려면 Sevlet 에서 코드를 작성해서 확인해봐야겠지만, 그 전에 @Entity랑 @Configurable이 같이 묶여도 이상이 없다는 것은 확인하고 넘어가야겠기에 테스트를 해봤습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={“file:web/WEB-INF/applicationContext.xml”})
public class MemberTest {

    @Test
    public void injectionTest() throws Exception {
        Member member = new Member();
        assertNotNull(member);
        assertNotNull(member.getMemberRepository());
        assertNotNull(member.getMemberRepository().getSessionFactory());
    }

}

물론 이 테스트를 돌릴 때에도 -javaagent 옵션을 주셔야 합니다. 이 경우에는 junit에 주어야겠죠.

사용자 삽입 이미지
테스트는 통관합니다. 설정은 다음과 같습니다.

    <tx:annotation-driven />

    <context:load-time-weaver />

    <context:spring-configured />

    <context:component-scan base-package=”domain” />

    <bean id=”member” class=”domain.Member” abstract=”true”
        scope=”prototype” p:member-dao-ref=”memberDao” />

이밖에도 sessionFactory, datasource, transactionManager 가 빈으로 등록되어 있지만 생략하겠습니다.

어쨋든 조금 쉬었다가 Servlet에서 위의 코드를 실행해보겠습니다.

@Configurable 사용하기

이제야… 이 글을 올릴 수 있게 됐네요. ㅎㅎㅎ 저번 주부터 올리고 싶었던 글인데, 별것도 아닌거 가지고 삽질을 하느라고 늦어졌습니다.

@Configurable이 뭔지, 왜 사용해야 하는지 궁금하시면 먼저 토비님이 마소에 기고하셨던 글인 “스프링프레임워크와 DDD(Driven Driven Design)”와 그 글을 보고 감명을 받은 찬욱군의 도메인 개체가 빈으로 선언되야 하는 걸까?와 제가 쓴 @Configurable을 사용해야 하는 이유를 읽으시는게 좋습니다.

1. 도메인 객체 구현. + 2. @Configurable 설정.
2. XML 설정.
3. java 아규먼트 설정.
4. 테스트.

0 순위가 빠졌네요. 테스트 코드부터 작성하겠습니다.

new를 사용해서 도메인 객체를 생성하고, 이 객체가 Repository라는 객체를 가지고 있나 확인하는 매우 간단한 코드입니다.

1. 도메인 객체 구현. + 2. @Configurable 설정.

구현은 간단합니다.

주목해서 봐야할 부분은 Repository 객체를 new 키워드로 생성하여 가지고 있지 않습니다. 스프링의 DI를  사용하여 주입받을 것이기 때문입니다.

그리고 클래스 선언 부 위에 @Configurable 애노테이션을 사용하여 이 객체를 스프링이 관리하도록 설정합니다. 괄호 안에는 스프링에서 이 객체를 나타낼 bean의 이름을 적어줍니다. 위와같이 클래스 명과 동일할 때는 생략해도 됩니다.

2.  XML 설정.

스프링 설정 파일은 다음과 같습니다.
gg

스프링 2.5에 새로 추가된 context 네임스페이스를 추가한 뒤, <context:load-time-weaver /> 엘리먼트와 <context:spring-configured /> 엘리먼트를 추가해줍니다. <context:spring-configured /> 이 녀석은 @Configurable이 붙어있는 bean을 찾아서 스프링이 관리하도록 설정합니다. 이 때 LTW(load time weaver)를 필요로 하는데, 이 녀석을 사용하도록 <context:load-time-weaver /> 엘리먼트를 등록해 줍니다.

그리고 member 객체는 명시적으로 컨테이너에서 만들 수 없도록 abstract 속성을 true로 해주고, 매번 다른 객체처럼 취급해야 하기 때문에 prototype Scope으로 설정해줍니다.

beans 엘리먼트에서 Autowiring 설정을 byName으로 해두었기 때문에, Member 클래스에 RepositoryImpl 클래스를 주입해 줄 것입니다.

3. java 아규먼트 설정.

Run -> Open Run Dialog를 클릭하고 JUnit을 우클릭하여 new를 선택하여 새로운 테스트 스크립트를 작성합니다.

이때 Test에서는 기존의 Test와 같은 값을 주고, Argument 탭에서 다음과 같이 -javaagent 옵션을 사용하여 spring-agent.jar 파일의 위치를 알려줍니다.

사용자 삽입 이미지
4. 테스트.
사용자 삽입 이미지
휴~ 이제 시작이네요. ㅎㅎ