Java

[Java]리플렉션(Reflection)

DevStory 2022. 5. 8.

리플렉션(Reflection)이란?

우리가 작성한 소스코드가 프로그램이 되기까지 컴파일러는 컴파일 타임에 소스코드가 문제없는지 확인합니다. 일반적으로 객체의 타입을 모르는데 특정 클래스의 메서드와 필드를 호출하는 경우 컴파일 에러가 발생하며, 특정 메서드와 필드를 호출하기 위해서는 해당 메서드와 필드가 존재하는 클래스를 타입으로 객체를 생생해야 합니다.

하지만, 프로그램을 개발하다 보면 객체의 타입은 모르지만 특정 클래스의 메서드 또는 필드를 호출해야 하는 경우가 있습니다. 이러한 상황에서 Reflection을 사용하여 문제를 해결할 수 있습니다.

Java는 리플렉션(Reflection)이라는 API를 제공하여 런타임(프로그램이 실행 중)에 객체의 타입을 몰라도 특정 클래스의 필드 또는 메서드를 호출할 수 있도록 합니다.


Reflection 프로세스

알 수 없는 객체(Unknown Object)가 존재하는 경우 Reflection API를 사용합니다. Reflection API를 사용하면 런타임에 클래스, 인터페이스, 생성자, 메서드 및 필드를 검사할 수 있으며 객체의 동작을 변경할 수 있습니다.

Java의 "java.lang" 또는 "java.lang.reflect" 패키지는 Reflection API를 사용하기 위한 클래스를 제공하며, "java.lang.Class" 패키지는 알 수 없는 객체의 클래스 정보를 접근할 수 있는 메서드와 속성을 제공합니다.


Reflection API가 사용되는 경우

  • 리플렉션은 주로 디버깅, JUnit과 같은 테스트 프레임워크에서 런타임에 알 수 없는 객체의 동작 과정을 분석하기 위해 사용됩니다.
  • Java에서 지원하는 라이브러리가 아닌 특정 기업의 라이브러리를 사용하는 경우 해당 라이브러리에 존재하는 클래스 및 메서드를 분석하는 경우 사용됩니다.

java.lang.reflect

  • 필드(Field): 필드 클래스에는 변수 또는 데이터 타입, 접근 지정자, 식별자 및 값과 같은 필드를 선언하는데 필요한 정보들이 존재합니다.
  • 메서드(Method): 메서드 클래스는 메서드의 접근 지정자, 반환 타입, 메서드 이름, 매개변수 타입, 예외 타입 등 메서드 정보들이 존재합니다.
  • 생성자(Constructor): 생성자 클래스에는 생성자의 접근 지정자, 생성자 이름 및 매개변수 타입 등 생성자 정보들이 존재합니다.
  • 수정자(Modifier): 수정자 클래스에는 클래스의 접근 지정자 정보가 존재합니다.

java.lang.Class

java.lang.Class 패키지는 런타임에 클래스 메타데이터(클래스의 정보)를 검색하거나 클래스의 동작을 검사하고 수정하기 위해 사용됩니다.

다음은 java.lang.Class의 객체를 생성하는 방법입니다. Person이라는 클래스가 존재한다고 가정합니다.

▶ 첫 번째 방법은 .class 확장자를 사용하는 것입니다.

Class obj = Person.class;

obj라는 객체는 Person 클래스에 대한 정보를 가지고 있으므로 Reflection을 수행할 수 있습니다.

▶ 두 번째 방법은 forName() 메서드를 사용하는 것입니다. forName() 메서드는 클래스의 이름을 인수로 받아 해당 클래스 이름의 객체를 반환합니다.

Class obj = Class.forName("Person");

인수로 넘긴 클래스 이름이 존재하지 않는 경우 ClassNotFoundException이 발생합니다.

▶ 세 번째 방법은 getClass() 메서드를 사용하는 것입니다.

Person person = new Person();
Class obj = person.getClass();

먼저 Person 클래스의 person이라는 객체를 생성합니다. 그런 다음 이 객체의 getClass() 메서드를 호출하여 해당 객체의 클래스 정보를 가져옵니다.


슈퍼 클래스 접근

클래스의 상위 클래스를 알고 싶은 경우 getSuperClass() 메서드를 사용합니다.

// 부모 클래스
public class Person { }

// 자식 클래스
public class Employee extends Person { }

public class Main{
  public static void main (String[] args) {
    Employee employee = new Employee();
    Class obj = employee.getClass();

    // Employee의 슈퍼 클래스
    Class superClass = obj.getSuperclass();
    
    System.out.println("슈퍼 클래스: " + superClass.getName());
  }
}

실행 결과

슈퍼 클래스: Person

Employee 클래스의 슈퍼 클래스는 Person이므로 getSuperClass() 메서드를 호출하면, Person 클래스의 정보가 반환됩니다.


인터페이스 접근

클래스가 어떤 인터페이스를 구현했는지 확인하고 싶은 경우 getInterfaces() 메서드를 사용하여 인터페이스 정보를 확인할 수 있습니다. 여러 개의 인터페이스를 구현했을 수 있으므로 배열로 반환됩니다.

interface Person { }
interface Company { }

public class Employee implements Person, Company {}

public class Main{
  public static void main (String[] args) {
    Employee employee = new Employee();
    Class obj = employee.getClass();

    Class[] objInterface = obj.getInterfaces();
    
    for(Class item : objInterface) {
      System.out.println("인터페이스 이름: " + item.getName());
    }
  }
}

실행 결과

인터페이스 이름: Person
인터페이스 이름: Company
반응형

Reflection API: 필드

위에서 이미 언급했듯이 java.lang.reflect 패키지는 필드 또는 클래스 멤버의 정보를 제공하는 Field 클래스를 제공합니다.

다음은 Reflection API를 사용하여 Employee 클래스의 필드 정보를 확인합니다. 필드의 접근 지정자가 public이라는 것을 주목해주세요.

public class Employee implements Person, Company {
  public String name;
}

public class Main{
  public static void main (String[] args) {
    try {
      Employee employee = new Employee();
      Class obj = employee.getClass();

      // name 필드 접근 후 값을 John으로 설정합니다.
      Field field1 = obj.getField("name");
      field1.set(employee, "John");

      // 필드의 값이 반환됩니다.
      String typeValue = (String) field1.get(employee);
      System.out.println("Value: " + typeValue);

      // 필드의 접근 지정자가 반환됩니다.
      int mod = field1.getModifiers();
      String modifier1 = Modifier.toString(mod);
      System.out.println("Modifier: " + modifier1);
      System.out.println(" ");
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

실행 결과

Value: John
Modifier: public


▶ getField() 메서드를 사용하여 public 필드 정보를 접근합니다.

Field field1 = obj.getField("name");


▶ 다음은 Method 클래스에서 제공하는 메서드입니다.

set() // 필드 값을 설정합니다.
get() // 필드 값을 반환합니다.
getModifiers() // 필드의 접근 지정자를 정수로 반환합니다.


위 예제는 Employee 클래스 필드의 접근 지정자를 public으로 선언했습니다. 다음은 Employee 클래스 필드의 접근 지정자를 private로 선언합니다.

public class Employee implements Person, Company {
  private String name;
}

public class Main{
  public static void main (String[] args) {
    try {
      Employee employee = new Employee();
      Class obj = employee.getClass();

      // private 필드를 접근합니다.
      Field field1 = obj.getDeclaredField("name");

      // private 필드의 값 수정을 허용합니다.
      field1.setAccessible(true);
        
      field1.set(employee, "John");

      // 필드의 값이 반환됩니다.
      String typeValue = (String) field1.get(employee);
      System.out.println("Value: " + typeValue);

      // 필드의 접근 지정자가 반환됩니다.
      int mod = field1.getModifiers();
      String modifier1 = Modifier.toString(mod);
      System.out.println("Modifier: " + modifier1);
      System.out.println(" ");
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

실행 결과

Value: John
Modifier: private

위 예제는 getFiled() 메서드가 아닌 getDeclaredField() 메서드를 호출하여 private 필드를 접근했습니다. 그런 다음 private 필드의 값을 수정하기 위해 setAccessible() 메서드에 true를 전달합니다.


Reflection API: 메서드

클래스에 있는 메서드에 정보를 접근하는 경우 Method 클래스를 사용합니다.

다음은 Reflection API를 사용하여 Employee 클래스의 메서드 정보를 확인합니다.

public class Employee implements Person, Company {
  public void work() {
    System.out.println("일하는 중...");
  }

  private void breakTime() {
    System.out.println("쉬는 중...");
  }
}

public class Main{
  public static void main (String[] args) {
    Employee employee = new Employee();
    Class obj = employee.getClass();

    // Employee 클래스의 메서드 정보가 반환됩니다.
    Method[] methods = obj.getDeclaredMethods();

    for (Method m : methods) {
      // 메서드 이름이 반환됩니다.
      System.out.println("Method Name: " + m.getName());

      // 메서드의 접근 지정자가 정수로 반환됩니다.
      int modifier = m.getModifiers();
      System.out.println("Modifier: " + Modifier.toString(modifier));

      // 메서드의 반환 타입이 반환됩니다.
      System.out.println("Return Types: " + m.getReturnType());
      System.out.println(" ");
    }
  }
}

실행 결과

Method Name: breakTime
Modifier: private
Return Types: void
 
Method Name: work
Modifier: public
Return Types: void


▶ getDeclaredMethods() 메서드를 사용하여 생성자 정보를 접근합니다. 여러 개의 메서드가 존재할 수 있으므로 배열 형태로 반환됩니다.

Method[] methods = obj.getDeclaredMethods();

해당 클래스와 슈퍼 클래스에 선언된 모든 생성자 정보를 접근하는 경우 getMethods() 메서드를 사용합니다.

▶ 다음은 Method 클래스에서 제공하는 메서드입니다.

getName() // 메서드의 이름을 반환합니다.
getModifiers() // 정수 타입으로 메서드의 접근 지정자를 반환합니다.
getReturnType() // 메서드의 반환 타입을 반환합니다.

Reflection API: 생성자

java.lang.reflect 패키지의 Constructor 클래스를 사용하여 클래스의 생성자 정보를 접근할 수 있으며 생성자를 확인할 수 있습니다.

다음은 Relfection API를 사용하여 Employee 클래스의 생성자 정보를 확인합니다.

public class Employee implements Person, Company {
  public Employee() {
  }

  public Employee(String name, int age) {
  }
}

public class Main{
  public static void main (String[] args) {
    Employee employee = new Employee();
    Class obj = employee.getClass();

    // Employee 클래스의 생성자 정보가 반환됩니다.
    Constructor[] constructors = obj.getDeclaredConstructors();

    for (Constructor c : constructors) {
      // 생성자 이름이 반환됩니다.
      System.out.println("Constructor Name: " + c.getName());

      // 생성자의 접근 지정자 정보가 정수로 반환됩니다.
      int modifier = c.getModifiers();
      String mod = Modifier.toString(modifier);
      System.out.println("Modifier: " + mod);

      // 각 생성자의 매개변수 수가 반환됩니다.
      System.out.println("Parameters: " + c.getParameterCount());
      System.out.println("");
    }
  }
}

실행 결과

Constructor Name: Employee
Modifier: public
Parameters: 0

Constructor Name: Employee
Modifier: public
Parameters: 2


▶ getDeclaredConstructors() 메서드를 사용하여 생성자 정보를 접근합니다. 여러 개의 생성자가 존재하므로 배열 형태로 반환됩니다.

Constructor[] constructors = obj.getDeclaredConstructors();

해당 클래스와 슈퍼 클래스에 선언된 모든 생성자 정보를 접근하는 경우 getCoustructors() 메서드를 사용합니다.

▶ 다음은 Constructor 클래스에서 제공하는 메서드입니다.

getName() // 생성자의 이름을 반환합니다.
getModifiers() // 정수 타입으로 생성자의 접근 지정자를 반환합니다.
getParameterCount() // 각 생성자에 있는 매개변수의 수를 반환합니다.

Reflection의 단점

  • 성능 오버헤드(Performance Overhead): Reflection은 유용한 기능입니다. 하지만 런타임에 Reflection API를 사용하여 알 수 없는 객체를 처리하는 과정은 성능적으로 좋지 않습니다. 따라서 Reflection을 사용하지 않는 방향으로 개발해야 합니다.
  • 보안 제한(Security Restrictions): Reflection은 컴파일 타임이 아닌 런타임 기능이므로 런타임 권한이 필요할 수 있습니다. 따라서 보안 때문에 Reflection를 사용한 소스코드가 실행할 수 없는 경우 무용지물이 될 수 있습니다.
  • 내부 노출(Exposure of Internals): Reflection을 사용하여 클래스의 메서드와 필드를 접근할 수 있습니다. 코드를 이식하지 않아도 되는 장점은 객체 지향 프로그래밍의 특징인 추상화를 위반합니다.

Reflection API는 런타임에 알 수 없는 객체를 처리할 수 있다는 장점이 있습니다. 하지만 장점에 비해 치명적인 문제들이 존재하므로 웬만하면 Reflection API를 사용하지 않는 것이 좋습니다.

반응형

댓글