@Configurable 사용시 버그 피해가기

Caused by: java.lang.VerifyError: (class:
머시기/모시기/클래스$$EnhancerByCGLIB$$6dd4e683, method: unique signature:
()L저기기/거시기/클래스;) Inconsistent stack height 1 != 0

참조 : http://forum.springframework.org/showthread.php?t=51455

CGLib으로 만든어진 객체에 위빙을 시도해서 생기는 문제 같은데, 일종의 버그인가봅니다. 이전에 @Configurable 테스트 할 땐 못 본 에러인데 오랜만에 이걸 쓸 일이 있어서 사용하니까 발생하네요.

이 문제를 해결(?)하려면 일단 META-INF 폴더를 클래스 패스 안에 만들어 줍니다. 만든 다음에 클래스 패스에 추가해도 되구요.

다음은 aop.xml 파일을 정의하고 다음과 같이 적어줍니다.

<?xml version=”1.0″?>

<!–
    AspectJ load-time weaving config file to install common Spring aspects.
–>
<aspectj>

    <weaver options=”-showWeaveInfo” >
        <exclude within=”*..*CGLIB*” />
    </weaver>

    <aspects>
        <aspect
            name=”org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect” />
        <aspect
            name=”org.springframework.transaction.aspectj.AnnotationTransactionAspect” />
    </aspects>

</aspectj>

다른 내용은 spring-aspects.jar 파일에 기본으로 들어있는 aop.xml의 내용과 일치하구요. 거기에 <warver> 엘리먼트를 추가해서 CGLIB이 들어간 클래스에는 위빙하지 말라는 설정을 해줍니다.

아.. 근데 이렇게 하면 프록시에는 위빙하지 말라는건데… 이것참 난감하네요. 버그 보니까 해결됐다고 나오는데 저 문제는 왜 발생하는건지..에구구…

@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 왜이리 안 되지;

http://forum.springframework.org/showthread.php?t=43690
위 링크에 있는 것과 똑같은 현상이 벌어지고 있습니다.

1. @Confiurable 애노테이션을 도메인 객체위에 붙이고…
2. XML 설정에서 해당 도메인 객체를 등록하고 이 때 scope을 prototype으로…
3. <context:spring-configured /> 추가해주고.. (@Configurable 붙은 녀석이 애플리케이션에서 생성될 때 스프링이 관리하도록…)
4. <context:load-time-weaver/> 추가해주고.. (LTW 사용해야 3번일을 할 수 있으니까..)
끝..

이렇게 하면

    @Test
    public void testDI() throws Exception {
        Member member = new Member();
        assertNotNull(member.getRepository());
    }

이 테스트가 통과해야 하는데..

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.context.weaving.AspectJWeavingEnabler#0’: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘loadTimeWeaver’: Initialization of bean failed; nested exception is java.lang.IllegalStateException: ClassLoader [sun.misc.Launcher$AppClassLoader] does NOT provide an ‘addTransformer(ClassFileTransformer)’ method. Specify a custom LoadTimeWeaver or start your Java virtual machine with Spring’s agent: -javaagent:spring-agent.jar

-javaagent:spring-agent.jar 옵션을 추가해서 실행시키라는 에러가 발생합니다.

그래서 이클립스의 Run -> Open Reun Dialog 클리갛고, JUnit 에서 우클릭 한다음 new로 새로 하나 만들고 아규먼트에 인자를 다음과 같이 줬습니다.

-javaagent:sd:\eclipse\workspace\spring2.5\lib\spring-agent.jar

하지만 에러 메시지는 동일합니다. OTL…
레퍼런스에서 Table 6.1. DefaultContextLoadTimeWeaver  LoadTimeWeavers 표를 보고 위버 설정 파일을 다음과 같이 수정했습니다.

<context:load-time-weaver weaver-class=”org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver”/>

자… 그랬더니 이번에는 레퍼런스를 보라는 에러가…ㄷㄷㄷ

java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.

@Configurable 사용해야 하는 이유

DTO를 공부하다가 들었던 의문.. 그동안 내가 만들었던 DO들의 빈약함(Anemic Domain Model)에 대해 명쾌하고 깔끔하게 정리를 해주신 글을 발견했습니다. 그리고 그 대안으로 Rich 또는 Smart DO 그리고 DDD로 이어지는 흐름도 좋은 글입니다. 머리가 맑아지는 동시에 공부할 것들로 머리가 꽉차게 되는 그런 글이기도 합니다.

스프링프레임워크와 DDD(Driven Driven Design)

기존에는 도메인 객체를 모든 레이어에 걸쳐서 사용했었습니다. 그런데 DDD를 하면 도메인이 서비스 계층과 리파지토리 계층 사이에 끼이게 됩니다. 이제 하나의 계층으로 자리를 잡게 되는 것이죠.
사용자 삽입 이미지출처: https://www.dbguide.net/know/know102001.jsp?mode=view&divcateno=9&divcateno_=9&pg=1&idx=3229

이렇게 되면 도메인 객체에 무언가(DAO나 Repository) 주입시켜주어야 합니다. 그런데.. 이 주입시키는 일 Dependency Injection을 사용하려면 스프링이 관리하는 bean이어야 합니다. 즉 스프링 설정 파일에 도메인 객체가 bean으로 등록되어 있어야 한다는 것이죠. 하지만 문제는 도메인 객체들은 bean으로 설정하여 종속성을 관리할 객체로는 적당하지 않습니다.

이 객체들의 생성을 스프링 컨테이너가 관리할 수 없습니다. 이 객체들은 애플리케이션 동작 중에 생성되기 때문에, 스프링이 생명주기를 관리할 bean으로 등록하여 사용한다는 것은 말이 안됩니다.

이럴 때 등장하는 녀석이 바로 저 @Configurable 애노테이션 입니다. 이녀석이 붙어있는 클래스를 스프링에 등록해두면, 스프링은 그 객체가 생성될 때 그 객체가 필요로 하는 bean들을 주입해 줍니다. 엄청나죠. 이런 일이 어떻게 가능할까요. 바로 AOP 때문에 가능하죠. method 호출 Joinpoint만을 지원하는 스프링 AOP로는 이런 일을 할 수 없습니다. 생성자 실행 Joinpoint를 지원하는 AspectJ가 할 수 있는 일이죠.

그래서 @Configurable을 사용하려면 AsepctJ Load Time Weaver가 필요합니다.

결국 스토리는 이렇게 됩니다.

  1. DDD를 하고싶어. (도메인 객체가 DAO를 필요로 하고 있어.)
  2. @Configurable을 사용해야 겠다. (애플리케이션 실행 도중 생기는 객체도 스프링에서 관리할꺼야.)
  3. LTW가 필요하구나.

아니면 간단하게 다음과 같이 해도 되죠.

  1. DDD를 하고싶어. (도메인 객체가 DAO를 필요로 하고 있어.)
  2. 도메인 생성자에서 new 써주지뭐.

아래의 스토리가 더 짧고 간단합니다. 그런데도 저는 첫 번째 스토리대로 하고 싶습니다. 왜냐면, 스프링이 도중에 생겨난 객체들 까지 관리하게 되면, 그 객체들(풍성해진 도메인)에 스프링의 DI와 AOP를 적용할 수 있기 때문이죠. 아래 스토리대로 하면, 도메인 객체가 참조하는 DAO가 바뀌면 소스코드를 매번 바꿔줘야 되겠죠. 그리고 다른 코드는 전부 DI랑 AOP 사용해놓고 도메인 계층만 왕따 시키는 것도 아니고.. 좀 그렇차나요.ㅎㅎ;

그래서.. 좀전까지 @Configurable과 LTW랑 팀을 맺고 저랑 2:1로 씨름을 하고 있었습니다. 제가 졌습니다. 오늘은..-_-;; 잘 안 되더라구요. Eclipse에서 JVM 아규먼트 설정해 주는게 틀렸나.. 왜 그러징;;