본문 바로가기

IT/Java

[Java] Javassist로 클래스의 의존성을 식별 할 수 있다.




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 이다.