[Java] 30. Reflection 기능을 사용하는 방법 - Method편


Study/Java  2020. 6. 5. 11:55

안녕하세요. 명월입니다.


이 글은 Java에서의 Reflection 기능을 사용하는 방법 - Method편에 대한 글입니다.


이전 글에서 Reflection 기능의 클래스를 설명한 적이 있습니다.

링크 - [Java] 29. Reflection 기능을 사용하는 방법 - Class편


Reflection의 기능은 클래스의 구조를 분석하는 기능이라고 설명했습니다.

그렇다면 Reflection로 동적 할당으로 클래스를 Object에 넣게 되면 Object의 기본 함수만 사용하는 것이 아니고 Reflection를 통해서 클래스의 구조를 분석해서 메소드를 검색하고 실행할 수 있습니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
// 테스트할 클래스  
class Node {
  // 생성자
  public Node() { }
  // 함수 생성
  public void print() {
    // 콘솔 출력
    System.out.println("Hello world");
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    try {
      // Node 클래스의 타입을 찾는다.
      Class<?> cls = Class.forName("Node");
      // Node 클래스의 생성자를 취득한다.
      Constructor<?> constructor = cls.getConstructor();
      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      Object node = constructor.newInstance();
      // Node 클래스의 print함수를 취득한다.
      Method method = cls.getMethod("print");
      // 취득한 함수에 생성한 인스턴스를 넣고 실행시킨다.
      method.invoke(node);
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위 예제에서 Node 클래스를 취득해서 인스턴스를 생성한다. 그리고 Node 클래스에서 print 메소드를 찾아서 메소드에 인스턴스를 넣고 실행하는 것입니다.

역시 main함수에서는 Node 키워드를 사용하지 않았기 때문에 Node 클래스를 완전히 독립적으로 나눌 수 있습니다.


Java에서는 메소드 성질의 함수가 두 종류밖에 없습니다. 생성자와 일반 함수입니다.

생성자의 경우는 반환형이 없는 함수의 형태입니다. Java에서는 getConstructor() 함수를 사용해서 취득할 수 있습니다.

일반 함수의 경우는 반환형이 있는 함수의 형태입니다. Java에서는 getMethod() 함수를 사용해서 취득할 수 있습니다.

이 함수들은 함수 이름도 중요하지만, 오버로딩의 기능이 있기 때문에 파라미터의 개수와 타입도 중요합니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
// 테스트할 클래스  
class Node {
  // 생성자
  public Node() {  }
  // 함수 생성 (파라미터가 없음)
  public void print() {
    // 콘솔 출력
    System.out.println("print");
  }
  // 함수 생성 (파라미터가 String 타입 하나 있음)
  public void print(String msg) {
    // 콘솔 출력
    System.out.println("print " + msg);
  }
  // 함수 생성 (파라미터가 String, int 타입 있음)
  public void print(String msg, int count) {
    // 콘솔 출력
    System.out.println("print " + msg + " count - " + count);
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    try {
      // Node 클래스의 타입을 찾는다.
      Class<?> cls = Class.forName("Node");
      // Node 클래스의 생성자를 취득한다.
      Constructor<?> constructor = cls.getConstructor();
      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      Object node = constructor.newInstance();
      // Node 클래스에서 파라미터가 없는 print함수를 취득한다. 
      Method method = cls.getMethod("print");
      // 취득한 함수에 생성한 인스턴스를 넣고 실행시킨다.
      method.invoke(node);
      // Node 클래스에서 String 파라미터를 가진 print함수를 취득한다.
      method = cls.getMethod("print", String.class);
      // 취득한 함수에 생성한 인스턴스와 파라미터 값을 넣고 실행시킨다.
      method.invoke(node, "test");
      // Node 클래스에서 String, int 파라미터를 가진 print함수를 취득한다.
      method = cls.getMethod("print", String.class, Integer.TYPE);
      // 취득한 함수에 생성한 인스턴스와 파라미터 값을 넣고 실행시킨다.
      method.invoke(node, "test2", 100);
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위처럼 같은 이름의 함수라도 파라미터의 설정에 따라 호출을 다르게 할 수도 있습니다. getConstructor함수도 똑같은 방식으로 생성자를 탐색해서 실행할 수 있습니다.


Class부분에서도 String타입으로 클래스를 검색해서 인스턴스를 생성할 수 있었습니다. Method도 같은 방식으로 String으로 메소드를 찾아서 실행 할 수 있습니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
// 테스트할 클래스  
class Node {
  // 멤버 변수
  private String data = "init";
  // 생성자
  public Node() {
    // 콘솔 출력
    System.out.println("Node() constructor");
  }
  // 생성자 String 파라미터가 있다.
  public Node(String data) {
    // 멤버 변수에 입력
    this.data = data;
    // 콘솔 출력
    System.out.println("Node(String) constructor");
  }
  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("data - " + data);
  }
  // 멤버 변수 값 수정 함수
  public void setData(String data) {
    // 멤버 변수 값 수정
    this.data = data;
  }
  // 멤버 변수 값 반환 함수
  public String getData() {
    // 멤버 변수 값 반환
    return this.data;
  }
}
public class Example {
  // 클래스 취득 함수
  private static Object getClass(String name, Object... args) {
    try {
      // Class.forName의 함수를 사용해서 문자열로 Class<?> 타입을 취득해 올 수 있다.
      Class<?> clz = Class.forName(name);
      // 파라미터를 가변으로 받았기 때문에 파라미터의 데이터 타입을 구해야 한다.
      Class<?>[] params = new Class<?>[args.length];
      // 파라미터의 개수만큼
      for (int i = 0; i < args.length; i++) {
        // 타입 추출
        params[i] = args[i].getClass();
      }
      // String으로 취득한 클래스 타입으로 생성자를 취득합니다.
      Constructor<?> constructor = clz.getConstructor(params);
      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      return constructor.newInstance(args);
    } catch (Throwable e) {
      return null;
    }
  }
  // 함수 실행 함수
  private static <T> T executeMethod(Object instance, String name, Object... args) {
    try {
      // 파라미터를 가변으로 받았기 때문에 파라미터의 데이터 타입을 구해야 한다.
      Class<?>[] params = new Class<?>[args.length];
      // 파라미터의 개수만큼
      for (int i = 0; i < args.length; i++) {
        // 타입 추출
        params[i] = args[i].getClass();
      }
      // 인스턴스의 클래스 타입에서 메소드를 찾는다.
      Method method = instance.getClass().getMethod(name, params);
      // 실행
      return (T) method.invoke(instance, args);
    } catch (Throwable e) {
      return null;
    }
  }
  // 실행 함수
  public static void main(String... args) {
    // 클래스를 받아온다.
    Object instance = getClass("Node", "hello world");
    // print 함수 실행
    executeMethod(instance, "print");
    // setData 함수 실행, 파라미터가 있기 때문에 값을 넣는다.
    executeMethod(instance, "setData", "good");
    // getData 함수를 실행해서 멤버 변수를 받는다.
    String data = executeMethod(instance, "getData");
    // 콘솔 출력
    System.out.println("Node data - " + data);
  }
}

위 예제처럼 String 타입으로 함수를 찾아서 실행하는 것이 가능합니다.

이 뜻은 무엇이냐면 프로그램의 실행 순서를 프로그램 안에서 결정하는 것이 아니고 환경 변수 등을 이용해서 프로그램 외부에서 클래스의 실행 순서를 결정할 수 있게 할 수 있습니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
// 테스트할 클래스  
class Node {
  // 생성자
  public Node() {
  }
  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("print");
  }
  // 실행 함수
  public void execute() {
    // 콘솔 출력
    System.out.println("execute");
  }
}
public class Example {
  // 클래스 취득 함수
  private static Object getClass(String name, Object... args) {
    try {
      // Class.forName의 함수를 사용해서 문자열로 Class<?> 타입을 취득해 올 수 있다.
      Class<?> clz = Class.forName(name);
      // 파라미터를 가변으로 받았기 때문에 파라미터의 데이터 타입을 구해야 한다.
      Class<?>[] params = new Class<?>[args.length];
      // 파라미터의 개수만큼
      for (int i = 0; i < args.length; i++) {
        // 타입 추출
        params[i] = args[i].getClass();
      }
      // String으로 취득한 클래스 타입으로 생성자를 취득합니다.
      Constructor<?> constructor = clz.getConstructor(params);
      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      return constructor.newInstance(args);
    } catch (Throwable e) {
      return null;
    }
  }
  // 함수 실행 함수
  private static <T> T executeMethod(Object instance, String name, Object... args) {
    try {
      // 파라미터를 가변으로 받았기 때문에 파라미터의 데이터 타입을 구해야 한다.
      Class<?>[] params = new Class<?>[args.length];
      // 파라미터의 개수만큼
      for (int i = 0; i < args.length; i++) {
        // 타입 추출
        params[i] = args[i].getClass();
      }
      // 인스턴스의 클래스 타입에서 메소드를 찾는다.
      Method method = instance.getClass().getMethod(name, params);
      // 실행
      return (T) method.invoke(instance, args);
    } catch (Throwable e) {
      return null;
    }
  }
  // 실행 함수
  public static void main(String... args) {
    if (args.length > 0) {
      // 클래스를 받아온다.
      Object instance = getClass("Node");
      executeMethod(instance, args[0]);
    }
  }
}

저는 간단하게 실행 함수의 파라미터를 받았지만, json이나 기타 환경 설정 파일로 설정이 가능합니다.


Reflection에서는 단순히 getMethod를 통해서 찾는 것이 아니라 getMethods를 통해서 클래스에 선언되어 있는 클래스를 전부 취득할 수도 있습니다.

import java.lang.reflect.Method;
// 테스트할 클래스
class Node {
  // 생성자
  public Node() { }
  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("print");
  }
  // 실행 함수
  public void execute() {
    // 콘솔 출력
    System.out.println("execute");
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    try {
      // Class.forName의 함수를 사용해서 문자열로 Class<?> 타입을 취득해 올 수 있다.
      Class<?> clz = Class.forName("Node");
      // 콘솔 출력
      System.out.println("*** method list ***");
      // Node 클래스의 메소드 취득하기
      for (Method method : clz.getMethods()) {
        // 메소드 이름 출력
        System.out.println(method.getName());
      }
      // 개행
      System.out.println();
      // 콘솔 출력
      System.out.println("*** getDeclaredMethods list ***");
      // Node 클래스의 메소드 취득하기
      for (Method method : clz.getDeclaredMethods()) {
        // 메소드 이름 출력
        System.out.println(method.getName());
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

방법으로는 getMethods함수와 getDeclaredMethods 함수를 이용하는 것입니다.

이 두 함수의 차이는 getMethods함수는 Node 클래스의 상위 클래스, 즉, Object에서 부터 Node클래스까지 있는 모든 함수를 찾는 것이고 getDeclaredMethods는 Node 클래스에만 있는 함수를 찾는 것입니다.

상황에 맞게 찾아서 사용하면 됩니다.


그리고 Reflection에는 좀 더 재미있는 기능이 있는데 private이나 protected처럼 접근에 제한되어 있는 함수도 실행할 수 있습니다.

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
// 테스트할 클래스  
class Node {
  // 생성자
  public Node() { }
  // 출력 함수 (private로 외부에서는 접근이 제한됨)
  private void print() {
    // 콘솔 출력
    System.out.println("print");
  }
  // 실행 함수 (private로 외부에서는 접근이 제한됨)
  private void execute() {
    // 콘솔 출력
    System.out.println("execute");
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    try {
      // Class.forName의 함수를 사용해서 문자열로 Class<?> 타입을 취득해 올 수 있다.
      Class<?> clz = Class.forName("Node");
      // Node 클래스의 생성자를 취득한다.
      Constructor<?> constructor = clz.getConstructor();
      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      Object node = constructor.newInstance();
      // 함수를 취득한다.
      for (Method method : clz.getDeclaredMethods()) {
        // 접근 제한자 무시하고 접근이 가능하게 한다.
        method.setAccessible(true);
        // 실행
        method.invoke(node);
      }
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

위처럼 Node 클래스에서 함수가 private로 되어 있어도 접근이 가능합니다.

이 방법은 보통 UnitTest에서 함수의 값을 확인할 때 사용하는 방법으로 실제 프로그램에서 사용하게 되면 OOP의 캡슐화 특성이 없어지기 때문에 권장하지는 않습니다.


여기까지 Java에서의 Reflection 기능을 사용하는 방법 - Method편에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.