Spring 3.0 (40) Spring ASM 모듈의 소스는 어디에?

오랜만에 쓰는 스프링 3.0 얘기. RC1의 출시예정일을 20일을 훌쩍 넘겨버렸지만, 아직 RC1의 릴리스는 언제쯤 될지 예측하기도 쉽지 않다. 모든 개발할 내역이 이슈트래커에 올라오는 것은 아니지만, 어쨌든 이슈트래커의 RC1 버전을 기준으로 보자면 남은 이슈는 130여개. 날이 갈 수록 줄기는 커녕, 오히려 늘어나고 있다. 계속 새로운 이슈가 등장하기 때문이겠지. 물론 이미 클리어된 것도 90쯤 된다.

아마 이러다가 더 이상 시간을 끌기가 마케팅적으로 좋지 않다고 판단될 즈음에 상당수 이슈가 final로 넘어가면서 RC1이 갑자기 나올 수도 있다. M2에서 그랬던 것처럼 말이다. 또는 RC2가 하나 더 나올지도 모르겠는데, 글세.

어쨌든 최근에는 changelog도 업데이트 잘 안되고 있다. SVN로그를 보면 주로 웹 관련된 사항이 많이 수정되고 있는 듯. 코드를 다듬고 테스트를 추가하는 등도 많이 있는 것으로 봐서 버그 수정도 많은 것 같고.

과연 3.0 정식버전은 언제 출시될 것인가. 이렇게 질질 끌다가 어느 순간 후다닥 마무리져서 나올지도 모른다. 아니면 더욱 질질 끌 수도. 2.1을 2.5로 바꿔가면서 거의 7-8개월이나 일정을 연기한 것처럼.

 

오늘 얘기는 org.springframework.asm 모듈.

3.0 초기에 아직 annotated factory method(@Configuration)가 나오기 전에 JavaConfig 프로젝트 3.0에서 돌려보려고 했더니 asm 관련 클래스를 찾을 수 없다고 오류가 났다. 결국 JavaConfig은 2.5랑 돌릴 수 밖에 없었다. 왜 그랬을까? 구버전 호환성을 자랑하는 스프링에 무슨 일이 있었을까.

3.0은 모듈을 기존보다 더욱 상세하게 무려 19개로 쪼갰다.

그 중에는 org.springframework.asm이라는 이름의 모듈이 있다. 그런데 정작 최신 스프링 소스를 받아서 이 모듈의 프로젝트를 열어보면 소스와 테스트 폴더가 모두 텅 비어있다. 그런데 다른 모듈(context 등등)에 보면 이 org.springframework.asm 모듈을 사용하는 곳이 있다. 그 프로젝트에 라이브러리 항목에서 org.springframework.asm.jar 파일을 열어보면 그 안에 클래스가 들어 있다. 이게 어찌된 일인가? 모듈 프로젝트에는 소스가 없는데, 그 빌드된 바이너리 파일에는 클래스가 들어있다. 물론 패키지가 org.springframework.asm으로 당당히 잡혀있는 클래스들이다. 이는 어찌된 일인가?

 

ObjectWeb의 ASM 라이브러리는 클래스 바이트코드 조작과 분석을 위한 라이브러리이다. 스프링은 빈 스캐너에서나 메소드의 변수 이름을 읽는 등의 용도로 이를 사용한다. ASM의 장점은 클래스를 로딩하지 않고도 클래스의 메타 정보를 읽을 수 있다는 점이라고 한다. 그래서 여기 저기서 제법 많이 사용된다.

문제는 CGLib과 하이버네이트와 사이에서 발생했다. 기존에도 스프링과 하이버네이트를 같이 사용하다보면 서로 비슷한 오픈소스 라이브러리를 사용하는 것을 자주 발견할 수 있었다. 그 중 자주 눈에 띄는 것이 바로 Enhancer를 이용해서 클래스에 다이나믹프록시를 쉽게 만들게 해주는 CGLib이다. CGLib가 이름(code generation lib.) 때문에 이를 무슨 ROO처럼 코드를 생성해주는 코드생성기 쯤으로 착각하고 있는 사람들도 많은 것 같은데, 그런 코드생성기와는 전혀 다른 개념이다.

아무튼 스프링은 클래스프록시 때문에 이 CGLib을 사용하고 하이버네이트도 역시 다양한 프록시를 쓰기 때문에 CGLib을 필요로 한다. 문제는 이런 식으로 같은 라이브러리를 사용하는 두개 이상의 다른 프레임워크를 쓸 때 서로 사용하는 라이브러리의 버전이 다른 경우가 발생할 수 있다는 점이다.

기존에 제기됐던 이슈는 스프링과 히이버네이트가 서로 다른 ASM라이브러리 버전을 사용하기 때문에 발생한 것이다. 스프링은 2.x, 하이버네이트는 1.x대를 사용하고 있던 때가 있었는데(지금은 모르겠다..) 문제는 ASM이 1과 2가 서로 클래스 이름은 같지만 바이너리 버전에서 호환이 안된다는 점이다. 같은 라이브러리를 사용하는데 마이너 버전만 좀 다르면, 보다 최신버전을 사용하는 쪽에 버전을 맡추면 대부분 문제가 해결된다. 문제는 ASM처럼 버전에 따라서 아예 기능이 서로 호환이 안되는 경우가 있다는 것이다.

결국 그 당시 스프링과 특정 버전의 하이버네이트를 같이 사용하면 어느 한쪽의 기능이 정상적으로 동작하지 않거나, 클래스를 찾을 수 없어서 오류가 나는 상황이 발생한다.

 

같은 클래스로더에 로딩되야 하는 프레임워크들 사이에 이렇게 바이너리 레벨의 호환이 안되는 라이브러리의 버전충돌 문제가 생기는 것은 참 난처한 일이다. OSGi처럼 수평적인 네트워크 구조의 클래스로딩 방식을 가지는 경우에는 라이브러리의 버전 충돌 문제를 근본적으로 회피할 수 있지만, 계층적인 종속 구조를 가지는 기존 자바 플랫폼에서는 일단 불가능하다.

그래서 이런 문제를 해결하기 위해서 사용하는 방법이 repacking이다. 뭐냐면 아예 버전이 다른 한쪽을 자바패키지를 통채로 바꾸는 것이다. 클래스의 이름과 내용은 같더라도 패키지가 다르면 다른 클래스로 인식하고 함께 사용할 수 있기 때문이다.

그래서 종종 충돌이 발생하는 라이브러리의 경우 이렇게 재패키지화 하는 경우가 많이 있다.

ASM버전 문제에 잘 낑겨들어가는 CGLib의 경우는 그래서 cglib_nodeps.jar라고 재패키징한 라이브러리도 종종 찾아볼 수 있다. ASM라이브러리를 cglib의 패키지 아래로 모두 옮기고, cglib에서 ASM의 기능을 사용할 때는 그렇게 패키지를 바꾼 라이브러리를 사용하게 하는 것이다. 결국 cglib.jar –> asm.jar 해야 할 것을 asm.jar가 필요없이 그냥 cglib_nodeps.jar를 사용하면 되게 하는 것이다.

그래서 스프링이나 하이버네이트의 과거 라이브러리 사용흔적을 잘 살펴보면 이 cglib_nodeps가 종종 등장했다.

문제는 cglib이 사용하는 ASM 에서 충돌이 생긴거라면 상관없지만, 스프링이나 하이버네이트가 직접 사용하는 ASM이 있을 경우가 있어서 문제가 생기는 경우가 있다는 점이다. 이때문에 서로 개발자들 간에 라이브러리 문제를 해결하기 위해서 잘 조율을 했으면 좋겠지만, 뭐 니가 고쳐라 이런 식으로 가면 또 쉽지 않다.

결국 스프링 팀은 자주 문제가 될 것으로 판단되는 이 ASM라이브러리를 스프링에서 본격적으로 사용하기 시작하면서, 아예 이를 재패키징 하기로 결정했다. 문제는 재패키징 방법이 편하기는 하지만 라이브러리 사이즈를 불필요하게 증가시킨다는 점이다. 특히 특정 기능에서 사용하지만, Core모듈에 추가되야 하는 이 ASM이 문제다. 그래서 결국 이 재패키징 된 ASM 만 별도의 모듈로 제공하기로 했다. 그래서 필요하면 이를 사용하고 아니면 뺄 수 있도록 한 것이다. 물론 3.0 기능을 본격적으로 쓴다면 대부분 사용할 테지만.

그래서 org.springframework.asm모듈에는 소스가 없다. 대신 objectweb의 asm과 asm.common 두개의 라이브러리를 리패키징해서 클래스 패키지를 모두 org.springframework.asm으로 변경하고 이를 재포장해서 모듈 라이브러리로 만드는 것이다. 그러면 ASM의 클래스들이 필요한 경우 모두 이 org.springframework.asm 밑의 클래스를 사용하기만 하면 된다. 이렇게 해서 충돌 문제는 모두 해결.

 

이 재패키징은 그럼 어떻게 할까?

그건 Jar Jar Links라는 Jar 재포장 라이브러리를 이용한다.

asm모듈의 ivy 빌드파일에 보면 다음과 같은 부분이 나온다.

<dependencies>
        <dependency org="org.objectweb.asm" name="com.springsource.org.objectweb.asm" rev="2.2.3" conf="jarjar->runtime"/>
        <dependency org="org.objectweb.asm" name="com.springsource.org.objectweb.asm.commons" rev="2.2.3" conf="jarjar->runtime"/>
</dependencies>

 

이 두개의 deps는 jarjar를 통해서 런타임 스코프를 가지는 라이브러리로 사용한다는 내용이다.

그리고 ANT빌드파일을 보면

<jarjar destfile="${jar.output.file}" index="true" filesetmanifest="merge">
            <manifest>
                <attribute name="Bundle-ManifestVersion" value="2"/>
                <attribute name="Bundle-Version" value="${bundle.version}"/>
                <attribute name="Implementation-Title" value="${implementation.title}"/>
                <attribute name="Implementation-Version" value="${implementation.version}"/>
            </manifest>
            <zipfileset src="${target.dir}/jarjar-staging/com.springsource.org.objectweb.asm.jar"/>
            <zipfileset src="${target.dir}/jarjar-staging/com.springsource.org.objectweb.asm.commons.jar"/>
            <rule pattern="org.objectweb.asm.**" result="org.springframework.asm.@1"/>
</jarjar>

이 jarjar를 사용한 곳이 나오는데 rule pattern을 보면 org.objectweb.asm의 클래스들을 org.springframework.asm으로 변경할 수 있도록 설정되어있다.

 

JavaConfig은 2.5 방식대로 내부적으로 ASM을 사용하고 있고, 아직 재포장된 org.springframework.asm에 호환되게 바뀌지 않았기 때문에 3.0에서 사용할 수 없었던 것이다.

 

최신 하이버네이트 라이브러리를 보면 문제가 자주 되는 cglib과 asm등을 아예 몽땅 하이버네이트 패키지로 리패키징해버렸다. hibernate-cglib-repack-2.1_3.jar 을 보면 알 수 있다. ASM은 물론이고 cglib까지 org.hibernate 밑으로 넣어버렸으니 이제 충돌날 걱정은 없다. 클래스가 좀 많아지고 전체 바이너리 사이즈가 조금 커졌을 뿐.

 

오늘의 결론.

1. 스프링의 asm 모듈에 소스가 안보인다고 SVN checkout을 잘못받았나 당황하지 말자.

2. 라이브러리 버전 충돌나면  jarjar의 도움으로 리패키징을 해보자.

3. 그것도 귀찮으면 OSGi를 써서 버전문제를 해결보자.

4. 제일 중요한 결론. 버전 올라가면서 바이너리 레벨에서 호환안될 거면 클래스 이름이나 패키지좀 바꿔라!

1 Comment

박성철July 1st, 2009 at 1:54 pm

결론에 감동했습니다. ㅎㅎ

Leave a comment

Your comment