Using Java Reflection

2024. 5. 27. 10:15High Level Programming Language/Learning the Java Language

Using Java Reflection

 

리플렉션은 자바 프로그래밍 언어의 기능 중 하나입니다. 이를 통해 실행 중인 자바 프로그램이 스스로를 검사하거나 "Introspect"하고, 프로그램의 내부 속성을 조작할 수 있습니다. 예를 들어, 자바 클래스가 모든 멤버의 이름을 얻어 이를 표시하는 것이 가능합니다.

"Introspect"는 프로그램이 실행 중에 자기 자신을 검사하고 그 내부 구조나 속성에 대해 정보를 얻는 능력을 의미합니다. 자바에서 리플렉션을 통해 수행되는 이러한 자기 검사는 "introspection"이라고 부릅니다. 구체적으로 introspection은 다음과 같은 작업을 포함할 수 있습니다:
1. 클래스 구조 탐색: 클래스의 이름, 패키지, 부모 클래스, 구현된 인터페이스 등을 얻을 수 있습니다.
2. 멤버 정보 조회: 클래스의 필드, 메서드, 생성자 등의 이름과 타입, 접근 제어자 등을 확인할 수 있습니다.
3. 메서드 호출: 특정 객체의 메서드를 동적으로 호출할 수 있습니다.
4. 필드 접근 및 수정: 객체의 필드 값을 동적으로 읽고 쓸 수 있습니다.
예를 들어, introspection을 사용하면 다음과 같은 작업을 수행할 수 있습니다:
 특정 클래스의 모든 메서드 이름을 출력합니다.
 객체의 필드 값을 동적으로 변경합니다.
 런타임에 클래스의 특정 메서드를 호출합니다.
이러한 기능은 코드 작성 시점에는 알 수 없는 클래스나 객체와 상호작용할 필요가 있는 다양한 상황에서 유용합니다. 예를 들어, 플러그인 시스템, 시리얼라이제이션, 디버깅 도구, 객체-관계 매핑(ORM) 프레임워크 등에서 introspection이 사용됩니다.

 

자바 클래스가 자체적으로 내부를 검사하고 조작할 수 있는 능력은 별로 대단하게 들리지 않을 수 있지만, 다른 프로그래밍 언어에서는 이 기능이 전혀 존재하지 않습니다. 예를 들어, Pascal, C, 또는 C++ 프로그램에서는 해당 프로그램 내에 정의된 함수에 대한 정보를 얻을 방법이 없습니다.

리플렉션의 구체적인 용도 중 하나는 JavaBeans에서 찾을 수 있습니다. 소프트웨어 컴포넌트는 빌더 도구를 통해 시각적으로 조작될 수 있습니다. 이 도구는 리플렉션을 사용하여 동적으로 로드된 자바 컴포넌트(클래스)의 속성을 얻습니다.

 

A Simple Example

리플렉션이 어떻게 작동하는지 이해하기 위해, 다음 간단한 예제를 살펴보십시오:

import java.lang.reflect.*;

 
public class DumpMethods {
  public static void main(String args[])
  {
     try {
        Class c = Class.forName(args[0]);
        Method m[] = c.getDeclaredMethods();
        for (int i = 0; i < m.length; i++)
        System.out.println(m[i].toString());
     }
     catch (Throwable e) {
        System.err.println(e);
     }
  }
}

 

위 코드를 실행시킬 때, main 메소드의 파라미터 아규먼트 값으로 java.util.Stack 문자열을 전달합니다.

위 코드를 실행시키는 방법은 여기 를 참조하세요

실행 결과는 콘솔에 다음과 같은 결과가 출력됩니다.

public boolean java.util.Stack.empty()
public synchronized java.lang.Object java.util.Stack.peek()
public synchronized int java.util.Stack.search(java.lang.Object)
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()

 

즉, java.util.Stack 클래스의 메소드 이름들이 정규화된 파라미터 및 리턴 타입과 함께 나열됩니다.

이 프로그램은 class.forName을 사용하여 지정된 클래스[java.util.Stack]를 로드한 다음 getDeclaredMethods를 호출하여 클래스에 정의된 메서드 목록을 검색합니다. java.lang.reflect.Method는 단일 클래스 메소드를 나타내는 클래스입니다.

 

Setting Up to Use Reflection

리플렉션 클래스는 java.lang.reflect 패키지에 있습니다. 이 클래스를 사용하기 위해 따라야 할 세 가지 단계가 있습니다. 첫 번째 단계는 조작하려는 클래스의 java.lang.Class 객체를 얻는 것입니다. java.lang.Class는 실행 중인 Java 프로그램에서 클래스와 인터페이스를 나타내는 데 사용됩니다.

Class 객체를 얻는 한 가지 방법은 다음과 같이 하는 것입니다:

Class c = Class.forName("java.lang.String");

 

이를 통해 String 클래스의 Class 객체를 얻을 수 있습니다. 다른 접근 방법으로는 기본 타입에 대한 Class 정보를 얻기 위해 다음과 같이 사용할 수 있습니다:

Class c = int.class;
Class c = Integer.TYPE;

 

후자는 기본 타입에 대한 래퍼(Integer 등)의 미리 정의된 TYPE 필드를 접근합니다.

두 번째 단계는 getDeclaredMethods와 같은 메서드를 호출하여 클래스에 선언된 모든 메서드의 목록을 얻는 것입니다.

이 정보를 얻은 후, 세 번째 단계는 리플렉션 API를 사용하여 정보를 조작하는 것입니다. 예를 들어, 다음 순서로:

Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());

 

 

이 코드는 String에 선언된 첫 번째 메서드의 텍스트 표현을 출력합니다.

아래 예에서는 세 단계가 결합되어 리플렉션을 사용하여 특정 애플리케이션을 처리하는 방법에 대한 자체 포함된 예제를 제공합니다.

 

Simulating the instanceof Operator

클래스 정보를 확보한 후 다음 단계는 클래스 체에 대한 기본적인 질문을 하는 것입니다. 예를 들어, Class.isInstance 메서드를 사용하여 instanceof 연산자를 시뮬레이션할 수 있습니다.

class A {}

public class instance1 {
  public static void main(String args[])
  {
     try {
        Class cls = Class.forName("A");
        boolean b1 
          = cls.isInstance(new Integer(37));
        System.out.println(b1);
        boolean b2 = cls.isInstance(new A());
        System.out.println(b2);
     }
     catch (Throwable e) {
        System.err.println(e);
     }
  }
}

 

이 예에서는 클래스 A에 대한 클래스 객체가 생성된 다음 클래스 인스턴스 객체가 A의 인스턴스인지 확인하기 위해 검사됩니다. Integer(37)은 A의 인스턴스가 아니지만, new A()는 그렇습니다.

 

Finding Out About Methods of a Class

리플렉션의 가장 가치 있고 기본적인 용도 중 하나는 클래스 내에 어떤 메서드가 정의되어 있는지 알아내는 것입니다. 이를 위해 다음 코드를 사용할 수 있습니다:

import java.lang.reflect.*;

public class method1 {
  private int f1(Object p, int x) throws NullPointerException
  {
     if (p == null)
        throw new NullPointerException();
     return x;
  }

  public static void main(String args[])
  {
     try {
       Class cls = Class.forName("method1");

        Method methlist[] = cls.getDeclaredMethods();
        for (int i = 0; i < methlist.length; i++) {  
           Method m = methlist[i];
           System.out.println("name = " + m.getName());
           System.out.println("decl class = " + m.getDeclaringClass());
           Class pvec[] = m.getParameterTypes();
           for (int j = 0; j < pvec.length; j++)
              System.out.println("param #" + j + " " + pvec[j]);
           Class evec[] = m.getExceptionTypes();
           for (int j = 0; j < evec.length; j++)
              System.out.println("exc #" + j + " " + evec[j]);
           System.out.println("return type = " + m.getReturnType());
           System.out.println("-----");
        }
     }
     catch (Throwable e) {
        System.err.println(e);
     }
  }
}

 

프로그램은 먼저 method1에 대한 클래스 설명을 가져온 다음 getDeclaredMethods를 호출하여 클래스에 정의된 각 메서드에 대해 하나씩 Method 객체들에 대한 리스트를 검색합니다. 여기에는 public, protected, package 및 private 메소드가 포함됩니다. 프로그램에서 getDeclaredMethods 대신 getMethods를 사용하면 상속된 메서드에 대한 정보도 얻을 수 있습니다.

Method 객체들의 리스트를 얻은 후에는 파라미터 유형, 예외 유형 및 각 메소드의 리턴 유형에 대한 정보를 표시하기만 하면 됩니다. 기본 타입이든 클래스 타입이든 이러한 각 타입은 차례로 클래스 설명자로 표시됩니다. 프로그램의 출력은 다음과 같습니다.

name = f1
   decl class = class method1
   param #0 class java.lang.Object
   param #1 int
   exc #0 class java.lang.NullPointerException
   return type = int
   -----
   name = main
   decl class = class method1
   param #0 class [Ljava.lang.String;
   return type = void
   -----

 

Obtaining Information About Constructors

클래스의 생성자를 알아내는 데에도 비슷한 접근 방식이 사용됩니다. 예를 들어:

import java.lang.reflect.*;
        
   public class constructor1 {
      public constructor1()
      {
      }
        
      protected constructor1(int i, double d)
      {
      }
        
      public static void main(String args[])
      {
         try {
             Class cls = Class.forName("constructor1");
        
           	 Constructor ctorlist[] = cls.getDeclaredConstructors();
             
             for (int i = 0; i < ctorlist.length; i++) {
                   Constructor ct = ctorlist[i];
                   System.out.println("name = " + ct.getName());
                   System.out.println("decl class = " + ct.getDeclaringClass());

                   Class pvec[] = ct.getParameterTypes();
                   for (int j = 0; j < pvec.length; j++)
                      System.out.println("param #" + j + " " + pvec[j]);
                   Class evec[] = ct.getExceptionTypes();
                   for (int j = 0; j < evec.length; j++)
                      System.out.println("exc #" + j + " " + evec[j]);
                   System.out.println("-----");
              }
          }
          catch (Throwable e) {
             System.err.println(e);
          }
      }
   }

 

생성자는 실제 리턴 타입이 없기 때문에,  이 예제에서는 검색된 리턴 타입 정보가 없습니다.

 

이 프로그램이 실행되면 출력은 다음과 같습니다.

name = constructor1
   decl class = class constructor1
   -----
   name = constructor1
   decl class = class constructor1
   param #0 int
   param #1 double
   -----

 

Finding Out About Class Fields

클래스에 어떤 데이터 필드가 정의되어 있는지 알아내는 것도 가능합니다. 이를 위해 다음 코드를 사용할 수 있습니다.

import java.lang.reflect.*;
        
   public class field1 {
      private double d;
      public static final int i = 37;
      String s = "testing";
        
      public static void main(String args[])
      {
         try {
            Class cls = Class.forName("field1");
        
            Field fieldlist[] 
              = cls.getDeclaredFields();
            for (int i 
              = 0; i < fieldlist.length; i++) {
               Field fld = fieldlist[i];
               System.out.println("name
                  = " + fld.getName());
               System.out.println("decl class = " +
                           fld.getDeclaringClass());
               System.out.println("type
                  = " + fld.getType());
               int mod = fld.getModifiers();
               System.out.println("modifiers = " +
                          Modifier.toString(mod));
               System.out.println("-----");
            }
          }
          catch (Throwable e) {
             System.err.println(e);
          }
       }
   }

 

이 예는 이전 예와 유사합니다. 새로운 기능 중 하나는 Modifier를 사용하는 것입니다. 이는 필드 멤버에서 발견된 수정자를 나타내는 리플렉션 클래스입니다(예: "private int"). 수정자 자체는 정수로 표시되며 Modifier.toString은 "공식" 선언 순서에 따라 문자열 표현을 반환하는 데 사용됩니다(예: "static" 이전의 "final"). 프로그램의 출력은 다음과 같습니다.

name = d
   decl class = class field1
   type = double
   modifiers = private
   -----
   name = i
   decl class = class field1
   type = int
   modifiers = public static final
   -----
   name = s
   decl class = class field1
   type = class java.lang.String
   modifiers =
   -----

 

메소드와 마찬가지로 클래스에 선언된 필드(getDeclaredFields)에 대한 정보를 얻거나 슈퍼클래스에 정의된 필드에 대한 정보(getFields)도 얻을 수 있습니다.

 

Invoking Methods by Name

지금까지 제시된 예는 모두 클래스 정보 획득과 ​​관련되어 있습니다. 그러나 지정된 이름의 메서드를 호출하는 등 다른 방법으로 리플렉션을 사용하는 것도 가능합니다.

 

이것이 어떻게 작동하는지 보려면 다음 예를 고려하십시오.

import java.lang.reflect.Method;

public class InvokingMethodsByName {
	
	public int add(int a, int b)
    {
		return a + b;
    }
      
    public static void main(String args[])
    {
    	
    	try {
    		Class cls = Class.forName("InvokingMethodsByName");
    		Class partypes[] = new Class[2];
    		partypes[0] = Integer.TYPE;
    		partypes[1] = Integer.TYPE;
    		Method meth = cls.getMethod("add", partypes);
    		InvokingMethodsByName methobj = new InvokingMethodsByName();
    		Object arglist[] = new Object[2];
    		arglist[0] = new Integer(37);
    		arglist[1] = new Integer(47);
    		Object retobj = meth.invoke(methobj, arglist);
    		Integer retval = (Integer)retobj;
    		System.out.println(retval.intValue());
    	}
    	catch (Throwable e) {
    		System.err.println(e);
       }
    }

}

 

프로그램이 add 메소드를 호출하려고 하지만 실행 시간까지 add 메소드를 알 수 없다고 가정합니다. 즉, 메소드 이름은 실행 중에 지정됩니다(예를 들어 JavaBeans 개발 환경에서 이 작업을 수행할 수 있음). 위의 프로그램은 이를 수행하는 방법을 보여줍니다.

getMethod는 두 개의 정수 파라미터 유형이 있고 적절한 이름을 가진 클래스에서 메소드를 찾는 데 사용됩니다. 이 메소드가 발견되어 Method 객체에 캡처되면 해당 유형의 객체 인스턴스에서 호출됩니다. 메서드를 호출하려면 Integer 객체에 래핑된 기본 정수 값 37과 47을 사용하여 파라미터 목록을 구성해야 합니다. 리턴 값(84)도 Integer 객체로 래핑됩니다.

 

Creating New Objects

생성자 호출에 해당하는 메서드 호출은 없습니다. 왜냐하면 생성자를 호출하는 것은 새로운 객체를 생성하는 것과 동등하기 때문입니다. 정확히 말하자면, 새로운 객체를 생성하는 것은 메모리 할당과 객체 생성을 모두 포함합니다. 따라서 이전 예제에 가장 가까운 것은 다음과 같이 말하는 것입니다:

Class c = Class.forName("java.lang.String");
Constructor cons = c.getConstructor();
Object obj = cons.newInstance();

 

이 코드는 java.lang.String 클래스의 디폴 생성자를 사용하여 새로운 인스턴스를 생성하는 예제입니다. getConstructor 메서드는 특정 생성자를 가져오고, newInstance 메서드는 해당 생성자를 사용하여 새로운 객체를 만듭니다. 따라서 이전 예와 가장 가까운 내용은 다음과 같습니다.

import java.lang.reflect.Constructor;

public class CreatingNewObjects {
	
	public CreatingNewObjects()
    {
    }
      
    public CreatingNewObjects(int a, int b)
    {
       System.out.println(
         "a = " + a + " b = " + b);
    }
      
    public static void main(String args[])
    {
    	try {
    		Class cls = Class.forName("CreatingNewObjects");
    		Class partypes[] = new Class[2];
    		partypes[0] = Integer.TYPE;
    		partypes[1] = Integer.TYPE;
    		Constructor ct = cls.getConstructor(partypes);
    		Object arglist[] = new Object[2];
    		arglist[0] = new Integer(37);
    		arglist[1] = new Integer(47);
    		Object retobj = ct.newInstance(arglist);
    	}
    	catch (Throwable e) {
    		System.err.println(e);
    	}
    }
}

 

지정된 파라미터 타입을 처리하는 생성자를 찾고 이를 호출하여 객체의 새 인스턴스를 만듭니다. 이 접근 방식의 가치는 컴파일 시간이 아닌 실행 시간에 생성자 조회 및 호출을 통해 순전히 동적으로 이루어지는 것입니다.

 

Changing Values of Fields

리플렉션의 또 다른 용도는 객체의 데이터 필드 값을 변경하는 것입니다. 이 값은 실행 프로그램에서 필드를 이름으로 조회한 다음 해당 값을 변경할 수 있는 리플렉션의 동적 특성에서 다시 파생됩니다. 이는 다음 예에서 설명됩니다.

import java.lang.reflect.Field;

public class ChangingValuesofFields {
	
	public double d;
    
    public static void main(String args[])
    {
       try {
          Class cls = Class.forName("ChangingValuesofFields");
          Field fld = cls.getField("d");
          ChangingValuesofFields f2obj = new ChangingValuesofFields();
          System.out.println("d = " + f2obj.d);
          fld.setDouble(f2obj, 12.34);
          System.out.println("d = " + f2obj.d);
       }
       catch (Throwable e) {
          System.err.println(e);
       }
    }

}

 

이 예에서 d 필드의 값은 12.34로 설정되어 있습니다.

 

Using Arrays

리플렉션의 마지막 용도 중 하나는 배열을 만들고 조작하는 것입니다. Java 언어의 배열은 특수한 유형의 클래스이며 배열 참조는 객체 참조에 할당될 수 있습니다.배열이 어떻게 작동하는지 보려면 다음 예를 고려하십시오.

import java.lang.reflect.Array;

public class UsingArrays {
	
	public static void main(String args[])
    {
       try {
          Class cls = Class.forName(
            "java.lang.String");
          Object arr = Array.newInstance(cls, 10);
          Array.set(arr, 5, "this is a test");
          String s = (String)Array.get(arr, 5);
          System.out.println(s);
       }
       catch (Throwable e) {
          System.err.println(e);
       }
    }

}

 

이 예에서는 10개의 문자열 배열을 만든 다음 배열의 위치 5를 문자열 값으로 설정합니다. 값이 검색되어 표시됩니다.

다음 코드에서는 보다 복잡한 배열 조작을 보여줍니다.

package com.intheeast.reflection;

import java.lang.reflect.*;

public class array2 {
   public static void main(String args[])
   {
      int dims[] = new int[]{5, 10, 15};
      Object arr 
        = Array.newInstance(Integer.TYPE, dims);
     
      Object arrobj = Array.get(arr, 3);
      Class cls = 
        arrobj.getClass().getComponentType();
      System.out.println(cls.getTypeName());
      arrobj = Array.get(arrobj, 5);
      Array.setInt(arrobj, 10, 37);
     
      int arrcast[][][] = (int[][][])arr;
      System.out.println(arrcast[3][5][10]);
   }
}

 

이 예에서는 int의 5 x 10 x 15 배열을 만든 다음 배열의 위치 [3][5][10]을 값 37로 설정합니다. 여기서 다차원 배열은 실제로 배열의 배열이라는 점에 유의하세요. , 예를 들어 첫 번째 Array.get 이후 arrobj의 결과는 10 x 15 배열입니다. 이것을 다시 한 번 벗겨서 15개의 길이의 배열을 얻고 해당 배열의 10번째 슬롯은 Array.setInt를 사용하여 설정됩니다.

 

생성되는 배열의 타입은 동적이므로 컴파일 타임에 알 필요가 없습니다.

 

Summary

Java 리플렉션은 이름으로 클래스 및 데이터 구조에 대한 정보의 동적 검색을 지원하고 실행 중인 Java 프로그램 내에서 해당 정보를 조작할 수 있으므로 유용합니다. 이 기능은 매우 강력하며 C, C++, Fortran 또는 Pascal과 같은 다른 기존 언어에는 해당 기능이 없습니다.