본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[자바성능] 클래스 정보와 reflection

Reflection 관련 클래스들


 

자바 API에는 reflection이라는 패키지가 있습니다. 이 패키지에 있는 클래스들을 써서 JVM에 로딩되어 있는 있는 클래스와 메소드 정보를 읽어 올 수 있습니다. 주요 클래스로 어떤 것들이 있으며, 각 클래스에서 제공되는 메소드들은 아래와 같습니다.

 

 

Class 클래스

 

Class 클래스는 클래스에 대한 정보를 얻기에 좋은 클래스입니다. Class 클래스는 생성자가 따로 없습니다. ClassLoader 클래스의 defineClass() 메소드를 이용해서 클래스 객체를 만들 수도 있지만, 좋은 방법은 아닙니다. Object 클래스에 있는 getClass() 메소드를 이용하는 것이 일반적입니다.

 

String getName(): 클래스의 이름을 리턴한다.

Package getPackage(): 클래스의 패키지 정보를 패키지 클래스 타입으로 리턴한다.

Field[] getFields(): public으로 선언된 변수 목록을 Field 클래스 배열 타입으로 리턴한다.

Field getField(String name): public으로 선언된 변수를 Field 클래스 타입으로 리턴한다.

Field[] getDeclaredFields(): 해당 클래스에서 정의된 변수 목록을 field 클래스 배열 타입으로 리턴한다.

Field getDeclaredField(String name): name과 동일한 이름으로 정의된 변수를 Field 클래스 타입으로 리턴한다.

Method[] getMethods(): public으로 선언된 모든 메소드 목록을 Method 클래스 배열 타입으로 리턴한다. 해당 클래스에서 사용 가능한 상속받은 메소드도 포함된다.

Method getMethod(String name, Class... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 메소드를 Method 클래스 타입으로 리턴한다.

Method[] getDeclaredMethods(): 해당 클래스에서 선언된 모든 메소드 정보를 리턴한다.

Method getDeclaredMethod(String name, Class... parameterTypes): 지정된 이름과 매개변수 타입을 갖는 해당 클래스에서 선언된 메소드를 Method 클래스 타입으로 리턴한다.

Constructor[] getConstructors(): 해당 클래스에 선언된 모든 public 생성자의 정보를 Constructor 배열 타입으로 리턴한다.

Constructor[] getDeclaredConstructors(): 해당 클래스에서 선언된 모든 생성자의 정보를 Constructor 배열 타입으로 리턴한다.

int getModifiers(): 해당 클래스의 접근자(modifier) 정보를 int 타입으로 리턴한다.

String toString(): 해당 클래스 객체를 문자열로 리턴한다.

 

 현재 클래스의 이름을 알고 싶으면 보통 다음과 같이 사용할 수 있습니다. 단, 여기서 getName() 메소드는 패키지 정보까지 리턴해 줍니다. 클래스 이름만 필요할 경우에는 getSimpleName() 메소드를 사용하면 됩니다.

String currentClassName = this.getClass().getName();



Method 클래스


Method 클래스를 이용하여 메소드에 대한 정보를 얻을 수 있습니다. 하지만, Method 클래스에는 생성자가 없어서, Method 클래스의 정보를 얻기 위해서 Class 클래스의 getMethods() 메소드를 사용하거나 getDeclaredMethod() 메소드를 써야합니다. Method 클래스의 주요 메소드에 대해서 알아보면 다음과 같습니다.

 

Class<?> getDeclaringClass(): 해당 메소드가 선언된 클래스 정보를 리턴한다.

Class<?> getReturnType(): 해당 메소드의 리턴 타입을 리턴한다.

Class<?>[] getParameterTypes(): 해당 메소드를 사용하기 위한 매개변수의 타입들을 리턴한다.

String getName(): 해당 메소드의 이름을 리턴한다.

int getModifiers(): 해당 메소드의 접근자 정보를 리턴한다.

Class<?>[] getExceptionTypes(): 해당 메소드에 정의되어 있는 예외 타입들을 리턴한다.

Object invoke(Object obj, Object...args): 해당 메소드를 수행한다.

String toGenericString(): 타입 매개변수를 포함한 해당 메소드의 정보를 리턴한다.

String toString(): 해당 메소드의 정보를 리턴한다.



Field 클래스


Field 클래스는 클래스에 있는 변수들의 정보를 제공하기 위해서 사용합니다. Method 클래스와 마찬가지로 생성자가 존재하지 않으므로 Class 클래스의 getField() 메소드나 getDeclaredFields() 메소드를 써야 합니다. Field 클래스의 주요 메소드에 대해서 알아보면 다음과 같습니다.


int getModifiers(): 해당 변수의 접근자 정보를 리턴한다.

String getName(): 해당 변수의 이름을 리턴한다.

String toString(): 해당 변수의 정보를 리턴한다.

 

나머지 reflection 관련 클래스는 앞의  세 가지 클래스와 비슷하게 사용할 수 있습니다.




reflection 관련 클래스를 사용한 예



package com.perf.reflect.clas;

public class DemoClass {
    private String privateField;
    String field;
    protected String protectedField;
    public String publicField;

    public DemoClass() {}
    public DemoClass(String arg) {}

    public void publicMethod() throws java.io.IOException,Exception {}
    public String publicMethod(String s, int i) {
        return "s=" + s + " i=" + i;
    }
    protected void protectedMethod() {}
    private void privateMethod() {}
    void method() {}

    public String publicRetMethod() { return null; }
    public InnerClass getInnerClass() {
        return new InnerClass();
    }
    public class InnerClass {
    }
}


다음은 위 클래스를 점검하는 용도의 클래스입니다. 클래스 정보를 가져오는 방법은 다음과 같습니다.


package com.perf.reflect.clas;
import java.lang.reflect.*;
public class DemoTest {

    public static void main(String[] args) {
        DemoClass dc = new DemoClass(); // 점검 대상 클래스 객체

        DemoTest dt = new DemoTest();
        dt.getClassInfos(dc);
    }
    public void getClassInfos(Object clazz) {
        Class demoClass=clazz.getClass();
        getClassInfo(demoClass);
        //getFieldInfo(demoClass);
        //getMethodInfo(demoClass);
    }
    public void getClassInfo(Class demoClass) {
        String className = demoClass.getName();
        System.out.format("Class Name: %s\n", className);
        String classCanonicalName = demoClass.getCanonicalName();
        System.out.format("Class Canonical Name: %s\n", classCanonicalName);
        String classSimpleName = demoClass.getSimpleName();
        System.out.format("Class Simple Name: %s\n", classSimpleName);
        String packageName = demoClass.getPackage().getName();
        System.out.format("Package Name: %s\n", packageName);
        String toString = demoClass.toString();
        System.out.format("toString: %s\n", toString);
    }
}

이 부분은 클래스 정보만을 가져오는 부분입니다. 가장 먼저 main 메소드에서 확인할 클래스의 객체를 생성한 이후에 그 객체를 ClassInfos() 메소드에 전달하여 해당 클래스의 정보를 일습니다. 이 메소드는 클래스의 이름과 패키지 정보들을 확인합니다. 다음을 필드 정보를 읽는 부분입니다.


package com.perf.reflect.clas;
import java.lang.reflect.*;
public class DemoTest {

    public static void main(String[] args) {
        DemoClass dc = new DemoClass(); // 점검 대상 클래스 객체

        DemoTest dt = new DemoTest();
        dt.getClassInfos(dc);
    }
    public void getClassInfos(Object clazz) {
        Class demoClass=clazz.getClass();
        getClassInfo(demoClass);
        //getFieldInfo(demoClass);
        //getMethodInfo(demoClass);
    }
    public void getClassInfo(Class demoClass) {
        String className = demoClass.getName();
        System.out.format("Class Name: %s\n", className);
        String classCanonicalName = demoClass.getCanonicalName();
        System.out.format("Class Canonical Name: %s\n", classCanonicalName);
        String classSimpleName = demoClass.getSimpleName();
        System.out.format("Class Simple Name: %s\n", classSimpleName);
        String packageName = demoClass.getPackage().getName()l
        System.out.format("Package Name: %s\n", packageName);
        String toString = demoClass.toString();
        System.out.format("toString: %s\n", toString);
    }
}


여기서 가장 어려운 부분은 식별자 데이터를 가져오는 부분입니다. getModifiers 메소드에서는 int 타입으로 리턴을 하기 때문에 간단하게 변환을 하기가 어렵습니다. 그에 대비해서 Modifier 클래스에 static으로 선언되어 있는 Modifier.toString() 메소드가 있습니다. 이 메소드에 int 타입의 값을 보내면 식별자 정보를 문자열로 리턴하게 됩니다. 다음은 메소드 정보를 가져오는 부분입니다.


private void getMethodInfo(Class demoClass) {
    System.out.println("----------------");
    Method[] method1 = demoClass.getDeclaredMethods();
    Method[] method2 = demoClass.getMethods();
    System.out.format("Declared methods: %d, Methods: %d\n", method1.length, method2.length);
    for (Method met1: method1) {
        // method name info
        String methodName = met1.getName();
        // method modifier info
        int modifier = met1.getModifiers();
        String modifierStr = Modifier.toString(modifier);
        // method return type info
        String returnType = met1.getReturnType().getSimpleName();
        // method parameter info
        Class params[] = met1.getParameterTypes();
        StringBuilder paramStr = new StringBuilder();
        int paramLen = params.length;
        if(paramLen != 0) {
            paramStr.append(params[0].getSimpleName()).append(" arg");
            for(int loop = 1; loop < paramLen; loop++) {
                pramStr.append(",").append(params[loop].getName()).append(" arg").append(loop);
            }
        }
    }
    // method exception info
    Class exceptions[] = met1.getExceptionTypes();
    StringBuilder exceptionStr = new StringBuilder();
    int exceptionLen = exceptions.length;
    if (exceptionLen != 0) {
        exceptionStr.append("throws")append(exceptions[0].getSimpleName());
        for(int loop=1; loop < exceptionLen; loop++) {
            exceptionStr.append(",").append(exceptions[loop].getSimpleName());
        }
    }
    // print result
    System.out.format("%s %s %s(%s) %s\n", modifierStr, returnType, methodName, paramStr, exceptionStr);
    }
}


메소드 가져오는 부분에서 중요한 것은 예외나 매개변수를 처리한 부분입니다. 이 두가지 데이터는 일반적으로 하나가 아니기 때문에 위와 같이 반복하면서 해당 부분의 정보를 읽어와야 합니다.


Class Name: com.perf.reflect.clas.DemoClass

Class Canonical Name: com.perf.reflect.clas.DemoClass

Class Simple Name: DemoClass

Package Name: com.perf.reflect.clas

toString: class com.perf.reflect.clas.DemoClass

-----------------------------

Declared Fields: 4, Fields: 1

private String privateField

String field

protected String protectedField

public String publicField

-----------------------------

Declared methods: 7, Methods: 13

public String publicMethod(String arg, int arg1)

public void publicMethod() throws IOException,Exception

protected void protectedMethod()

private void privateMethod()

public String publicRetMethod()

public InnerClass getInnerClass()

void method()


클래스 정보를 가져오는 부분과 JMX를 연계시킨다면, 서버에서 사용하는 클래스정보를 가져오는 모니터링 기술 제공도 가능합니다.



reflection 클래스를 잘못 사용한 사례


일반적으로 로그를 프린트할 때 클래스 이름을 알아내기 위해서 다음과 같이 Class 클래스를 많이 사용하게 됩니다.


this.getClass().getName()


이 방법을 사용한다고 해서 성능에 많은 영향을 미치지는 않습니다. 다만 getClass를 할 때 Class 객체를 만들고, 그 객체의 이름을 가져오는 메소드를 수행하는 시간과 메모리를 사용할 뿐입니다. 다음은 reflection을 잘못 사용한 예입니다.


public String checkClass(Object src) {
    if(src.getClass().getName().equals("java.math.BigDecimal")) {
    }
}


해당 객체의 클래스 이름을 알아내기 위해서 getClass().getName() 메소드를 호출하여 사용했습니다. 이렇게 사용할 경우 응답 속도에 그리 많은 영향을 주지는 않지만, 많이 사용하면 필요 없는 시간을 낭비하게 됩니다. 이러한 부분이 필요할 때에는 다음과 같이 수정하는 것이 좋습니다.


public String checkClass(Object src) {
    if(src instanceof java.math.BigDecimal) {
        // 데이터 처리
    }
    // 이하 생략
}


instanceof를 사용하면 소스가 훨씬 간략해지게 됩니다. 이부분이 많이 호출된다면 성능면에서도 향상을 가져올 수 있습니다.



클래스의 메타 데이터 정보는 JVM의 Perm 영역에 저장됩니다. 만약 Class 클래스를 사용하여 엄청나게 많은 클래스를 동적으로 생성하는 일이 벌어지면 Perm 영역이 더이상 사용할 수 없게 되어 OutOfMemoryError가 발생할 수도 있습니다.