7. 클래스 정보, 어떻게 알아낼 수 있나?

de_sj_awa·2021년 8월 29일
0

7. 클래스 정보, 어떻게 알아낼 수 있나?

자바에는 클래스와 메서드의 정보를 확인할 수 있는 API가 있다. 바로 Class 클래스와 Method 클래스이다. Class 클래스와 Method 클래스가 성능에 얼마나 영향을 주는지 확인해보자.

1. reflection 관련 클래스들

자바 API에는 reflection이라는 패키지가 있다. 이 패키지에 있는 클래스들을 사용하면 JVM에 로딩되어 있는 클래스와 메서드를 정보를 읽어 올 수 있다. 주요 클래스의 종류와 각 클래스에서 제공되는 메서드에는 어떤 것들이 있는지 간단히 알아보자.

Class 클래스

Class 클래스는 클래스에 대한 정보를 얻을 때 사용하기 좋고, 생성자는 따로 없다. ClassLoader 클래스의 defineClass() 메서드를 이용해서 클래스 객체를 만들 수도 있지만, 좋은 방법은 아니다. 그보다는 Object 클래스에 있는 getClass() 메서드를 이용하는 것이 일반적이다. Class 클래스의 주요 메서드에 대해서 간단히 알아보자.

  • 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() : 해당 클래스 객체를 문자열로 리턴한다.

현재 클래스의 이름을 알고 싶으면 다음과 같이 사용하면 된다.

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

그런데, 여기서 getName() 메서드는 패키지 정보까지 리턴해 준다. 클래스 이름만 필요할 경우에는 getSimpleName() 메서드를 사용하면 된다.

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() 메서드나 getDeclareFields() 메서드를 써야 한다. Field 클래스의 주요 메서드에 대해서 알아보자.

  • int getModifiers() : 해당 변수의 접근자 정보를 리턴한다.
  • String getName() : 해당 변수의 이름을 리턴한다.
  • String toString() : 해당 변수의 정보를 리턴한다.

나머지 reflection 관련 클래스는 앞에서 설명한 세 가지 클래스와 비슷하게 사용할 수 있다.

2. 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 args) {}
  
    public void publicMethod() throws java.io.IOException, Exception {}
    public String publicMethoc(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.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

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);
    }
  
    public void getFieldInfo(Class demoClass) {
      System.out.println("-----------------------");
      Field[] field1 = demoClass.getDeclaredFields();
      Field[] field2 = demoClass.getFields();
      System.out.format("Declared Fields: %d, Fields: %d\n", field1.length, field2.length);
    
      for(Field field: field1) {
        String fieldName = field.getName();
        int modifier = field.getModifiers();
        String modifierStr = Modifier.toString(modifier);
        String type = field.getType().getSimpleName();
        System.out.format("%s %s %s\n", modifierStr, type, fieldName);
      }
    }
  
    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(" args");
          for (int loop = 1; loop < paramLen; loop++) {
            paramStr.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);
      }
    }
}

getClassInfo 메서드는 클래스 정보를 가져오는 부분이다.

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

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

클래스 정보를 가져오는 부분과 JMX를 연계시킨다면, 서버에서 사용하는 클래스의 정보를 가져오는 막강한 모니터링 기술을 제공할 수도 있을 것이다.

3. 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) {
        // 데이터 처리
    }
}

instancof를 사용하니 소스가 훨씬 간단해졌다. 그러면 JMH를 이용하여 얼마나 성능 차이가 있는지 비교해 보자.

package com.perf.reflection;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.GenerateMicroBenchmark;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class Reflection {

    int LOOP_COUNT = 100;
    String result;

    @GenerateMicroBenchmark
    public void withEquals() {
        Object src = new BigDecimal("6");
        for (int loop = 0; loop < LOOP_COUNT; loop++) {
            if (src.getClass().getName().equals("java.math.BigDecimal")) {
                result = "BigDecimal";
            }
        }
    }

    @GenerateMicroBenchmark
    public void withInstanceof() {
        Object src = new BigDecimal("6");
        for (int loop = 0; loop < LOOP_COUNT; loop++) {
            if (src instanceof java.math.BigDecimal) {
                result = "BigDecimal";
            }
        }
    }
}

수행 횟수는 10번이며, 앞서 살펴본 BigDecimal 객체인지 아닌지를 확인하는 코드다. 측정 결과는 다음과 같다.

대상 응답 시간(마이크로초)
instanceof 사용 0.167
Reflection 사용 1.022

instanceof를 사용했을 때와 .getClass().getName()을 사용했을 때를 비교하면 약 6배의 성능 차이가 발생한다. 어떻게 보면 시간으로 보았을 때 큰 차이는 발생하지 않지만, 이런 부분이 모여 큰 차이를 만들기 때문에 작은 것부터 생각하면서 코딩하는 습관을 가지는 것이 좋다.

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

참고

  • 자바 성능 튜닝 이야기
profile
이것저것 관심많은 개발자.

0개의 댓글