Javassist 는 꽤 유명한 Java Bytecode 라이브러리이다.
이것을 이용해서 사용자는 클래스를 동적으로(!) 변형해서 쓸 수 있다.
이걸 이용해서 흔히 말하는 AOP를 할 수도 있다.
뭐 그것들은 검색하면 다 나올 테니 여기서 언급하지는 않을 거고..
여기서는 현재 있는 클래스를 분석해서 클래스 의존성을 알아내는 방법 일부를 보이겠다.
의존성을 알아내는 행위는 엄청나게 큰 어플리케이션을
(업무 별로) 적당히 나누어 빌드 및 배포할 때 꽤 중요한데,
필요하다 싶은 모든 라이브러리나 컴포넌트를 모두 배포하면 너무 뚱뚱해서
자원이 부족하거나 기동 시간이 현실적이지 못하는 경우가 생기기 때문이다.
그럴 때는 그 어플리케이션에서 실제 쓰이는 컴포넌트를 식별해서
"이 컴포넌트를 포함하는 최소 셋"을 분리하는 작업이 있으면 좋다.
이걸 계산해 내는 구체적인 알고리즘은 일전에 소개한 문제와 유사하므로
(http://blog.naver.com/anabaral/130041666110)
여기서 자세히 다루지는 않겠다.
다만 여기서는 Javassist를 이용해서 의존성을 어떻게 밝혀낼 수 있는 지를 간단한 예제와 함께 보일 작정이다.
뭐.. 보통 의존성을 가릴 때는 가장 쉽게 할 수 있는 것이 소스를 분석해서 하는 것이지만,
소스 분석할 때는 그것대로 애로사항이 있다.
공백, 엔터, 괄호열고닫고, 문자열 등등을 적절히 해석해 주어야 하기 때문에
결국 소스 전문 Parser가 없으면 정확성을 100% 보장하지 못하는 경우가 많다.
여기서는 javassist를 이용해 이미 컴파일 된 클래스를 직접 해석하는 방식을 취하는데
또다른 애로사항이 있긴 하지만 장점도 존재하므로 택했다.
장점이 무엇인지는 짐작이 될 테니 패스.
...
일단 다음과 같은 가정을 하겠다.
(1) 컴포넌트들은 각각 *.jar 파일로 만들어져 있다.
(2) 한 컴포넌트 내에 다른 컴포넌트를 호출하는 클래스는 정해져 있다.
(3) 그 정해져 있는 클래스는 명명규칙을 통해 식별할 수 있다. (즉 이름으로 다른 클래스와 구분된다)
(4) 한 컴포넌트가 다른 컴포넌트를 호출할 때는 대상 컴포넌트가 가지는 특정 인터페이스를 이용한다.
(5) 호출용 인터페이스는 컴포넌트 안에 존재하며 명명규칙을 통해 식별할 수 있다.
위와 같은 가정을 하게 되면 일반적인 케이스가 아니므로
여기서 뭔가 바로 쓸 수 있는 코드를 얻어가려는 분들이 좀 당황스러울 지 모르겠다.
하지만 내가 처한 상황이 이러했으므로.. 다른 상황에 적용하려면 조금 더 연구해서 응용해 보시길..
예제를 단순하게 만들기 위해 하나의 가정을 더 포함시키겠다.
(6) 사실 "실제 쓰이는" 컴포넌트는 한 개이다. ( ^^; )
일단 다음과 같은 코드를 준비.
File jarFile = new File("jar file이 실제 존재하는 파일 경로");
ClassPool pool = new ClassPool();
pool.appendClassPath(jarFile.getAbsolutePath());
이제 이 컴포넌트 안의 모든 클래스들을 뒤져보자.
JarFile jarFile = new JarFile(jarFilePath);
Enumeration enu = jarFile.entries();
while (enu.hasMoreElements()) {
JarEntry je = (JarEntry) enu.nextElement();
String resourceName = je.getName();
// 이 resourceName을 가지고 여러 판별 작업을 할 것이다
if (resouceName으로 보니 "호출 하는" 클래스명이면) {
// className으로 변환
String className =
resourceName.substring(0,resourceName.length() - 6).replace('/', '.');
CtClass cc = pool.getCtClass(className);
Collection refs = cc.getRefClasses(); // Collection of String
for (Iterator it = refs.iterator(); it.hasNext();) {
String refClassName = (String) it.next();
if (refClassName 이 "호출 대상" 인터페이스명이면){
// 빙고! 이 컴포넌트가 무엇을 호출하는 지 발견했다.
}
}
// 다음 예제는 여기서 실행된다고 치자. 똑같은 코드 복사하기 귀찮아서..
}
}
위에 보면
한편으론 Jar 파일 내의 클래스들을 따로 뜯어보면서
다른 한편으론 Javassist에서 관리하는 풀 내의 클래스를 읽어 조사한다.
(native Java API가 getRefClasses() 같은 메소드를 지원하거나, Javassist가 jar파일 내의 모든 클래스들을 읽을 수 있게 해 준다면 둘을 병행할 이유는 없는데 그런 방법이 없어서, 혹은 내가 찾지 못해서 부득불 이런 방법을 사용했다)
만약 위와 반대로
주어진 컴포넌트에 대해서
그 컴포넌트를 호출하는 호출자 컴포넌트들을 찾고 싶다면
다음과 같이 해야 할 것이다.
1) 주어진 컴포넌트에 존재하는 "호출 대상" 인터페이스명을 모두 찾아서 알아둔다.
2) 추정되는 모든 컴포넌트들을 위에서와 같이 Jar 파일을 뒤져서
위에 알아두었던 "호출대상" 인터페이스들을 호출하는 지 여부를 조사한다.
3) 호출하는 인터페이스가 하나라도 있으면 빙고.
그런데 내 경우에는 한층 더 고난위의 요구가 들어왔다.
아래와 같이 컴포넌트 호출 관계가 있는데,
(A) ---> (B) ---> (C)
(B) ---> (D)
컴포넌트 B가 C와 D를 호출하기는 하지만, 이 중 A에서 호출하는 루트를 타고 호출하는 경우는 컴포넌트 C 뿐이다.
이럴 경우 배포대상에서 컴포넌트 D는 배제하고 싶다.
하지만 위에서처럼 컴포넌트 단위만으로만 호출관계를 따진다면 D도 포함이 된다.
이럴 때는 의존관계를 컴포넌트 단위가 아닌, 메소드 단위까지 내려가서 조사해야 한다.
전체적인 복잡한 로직은 뭐 잘 설계한다 치고,
여기서 기술적인 관심사는 다음과 같다.
1) 컴포넌트 A가 컴포넌트 B를 호출할 때, B의 어떤 메소드를 호출하는 지를 알 수 있을까?
2) 그리고 컴포넌트 B가 호출하는 대상을 식별할 때, B의 특정 메소드만을 조사할 수 있을까?
Javassist에 기본적인 API는 주어지므로 1)이 가능하다면 2)는 이를 기반으로 가능해진다.
이제 1)이 문제다.
Javassist가 1)을 지원 못 할 리는 없는데,
Javadoc 등의 설명이 이상하게 불충분해서 좀 찾는 데 애먹었다.
아무튼 아래 예제를 보면 감이 잡힐 것이다.
// 위의 예제에 언급한 위치에서 시작
CtMethod method = null;
CtMethod[] methods = cc.getMethods();
for(int i = 0; i < methods.length ; i++){
if (methods[i] 가 조사하고 싶은 메소드라면 )){ // 조건 2) 의 충족
method = cc.getMethods()[i];
MethodInfo info = method.getMethodInfo2();
ConstPool cpool = info.getConstPool();
CodeAttribute cattr = info.getCodeAttribute();
CodeIterator ci = cattr.iterator();
while(ci.hasNext()){ // 인스트럭션 bytecode 를 하나씩 분석한다.
int pos = ci.next();
if (ci.byteAt(pos) == Opcode.INVOKEINTERFACE){
// 인터페이스 호출 인스트럭션을 만남
int index = ci.u16bitAt(pos + 1);
String methodRefClassName =
cpool.getInterfaceMethodrefClassName(index);
String methodRefMethodName =
cpool.getInterfaceMethodrefName(index);
if (methodRefClassName 이 명명규칙에 해당되면)){
// 빙고! 호출되는 인터페이스명과 메소드명을 찾았다. 조건 1) 이 충족되었다.
}
}
}
}
}
나는 컴포넌트 간 호출이 특정 인터페이스를 통해서 이루어진다는 가정 하에서 조사하였으므로
INVOKEINTERFACE 라는 인스트럭션을 찾으면 충분했지만 사실 호출은 다음 네 가지 종류가 있다.
- INVOKEINTERFACE : 인터페이스 메소드 호출
- INVOKESPECIAL : 생성자 호출
- INVOKESTATIC : static 메소드 호출
- INVOKEVIRTUAL : 일반 클래스 메소드 호출
라고 한다.
뭐.. 현재 이에 대해 내가 더 아는 바는 거의 없다.
심도 깊은 연구를 하고 싶다면 JVM Reference 같은 걸 찾아보시라.
사족 하나만 달겠다.
위에 얘기했던 거..
인스트럭션을 하나하나 찾아서 분석하는 방법은
이상하게도 Javassist 튜토리얼이나 Javadoc에 설명이나 예제가 없거나 부족했다.
잘은 몰라도, 이런 류의 API는 제공되는 Javassist 버전 변경 시 변경이 일어날 가능성이 있을 수 있다.
메소드 이름이 바뀐다든지.. 구조 자체가 확 달라진다든지..
그러므로 일단 다음을 밝혀 둔다.
내가 테스트했던 Javassist 버전은 3.11.0.GA 이다.
[출처] Javassist 로 클래스 의존성 식별|작성자 우가가
'IT개발 > Java' 카테고리의 다른 글
[Java] HashMap 함수 제대로 알고 사용하기 (1) | 2014.03.28 |
---|---|
[Java] 자바 예약어 총정리 (1) | 2014.03.27 |
[Java] Static 키워드 바로 알고 사용하자 (15) | 2014.03.25 |
[Java] 해쉬맵(HashMap)에 대하여 심층적으로 알아보자 (0) | 2014.03.21 |
[Java] java.lang패키지와 String 클래스 (0) | 2014.01.23 |