본문 바로가기
Old Posts/Java

[Java] Java Reflection 소개 및 사용법, 예제

by A6K 2022. 8. 12.

자바 리플렉션(Java Reflection)은 구체적인 클래스의 타입을 알지 못하는 상황에서그 클래스에 구현되어 있는 메소드와 필드 등의 정보, 즉 클래스의 구조를 확인할 수 있도록 자바가 제공해주는 API다. 리플렉션은 자바에서 제공해주는 API이기 때문에 별도의 jar 파일을 포함하거나 Maven 의존성을 추가하지 않아도 사용할 수 있다.

일반적으로 자바 리플렉션은 많이 사용되지는 않는다. 다만 프레임워크(Frame Work) 소프트웨어를 작성하거나 IDE 같은 소프트웨어를 작성하는 경우 사용자가 나중에 어떤 타입의 클래스를 사용할지 소프트웨어를 작성하는 당시에는 알지 못하는 경우가 많이 있다. 이 때, 런타임에 사용자가 넘겨준 클래스의 정보를 분석해서 동작할 수 있도록 리플렉션을 이용해 코드를 작성할 수 있다.

예를 들어 데이터베이스를 다루는 소프트웨어를 작성한다고 생각해보자. Person 객체의 데이터는 tbl_person 테이블로 insert하고 Student 객체의 데이터는 tbl_student 테이블로 insert 하도록 코딩 컨벤션이 맞춰져 있다고 생각해보자. Person 객체와 Student 객체를 받는 메소드를 따로 만들어도 되지만 임의의 객체를 입력으로 받아 객체의 클래스 정보를 동적으로 확인해서 필드 내용을 조사해 런타임에 insert 구문을 만들어내는 메소드를 생성할 수도 있다. 후자의 경우 런타임에 클래스 정보를 확인해야하는데 이 때 리플렉션이 사용된다.

자바 리플렉션

자바 리플렉션에 대해 말로 알아보는 것보다는 예제 코드를 보면서 알아가는게 제일 편하다. 다음과 같은 클래스들의 정보를 리플렉션 API를 통해 접근해보겠다. (클래스들은 test.reflection 패키지에 정의했다.)

public interface Eating {
    String eats();
}

public abstract class Animal implements Eating {

    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public abstract String cry();
}

public interface Movable {
    String move();
}

public class Dog extends Animal implements Movable {

    private int age;

    public Dog(String name) {
        super(name);
    }

    @Override
    public String cry() {
        return "Bark";
    }

    @Override
    public String eats() {
        return "feed";
    }

    @Override
    public String move() {
        return "run";
    }

    public String wag() {
        return "wag tail";
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

간단히 클래스들의 관계를 설명하자면

Animal 클래스는 Eating 인터페이스를 구현한 abstract 클래스다. 이를 상속받아서 Dog 클래스 구현되는데 Dog 클래스는 Movable 인터페이스를 구현하고 있는 상태다.

클래스 정보

자바 리플렉션은 Class 라는 이름의 클래스를 통해 클래스 정보들을 제공해준다. 특정 클래스의 정보에 접근하기 위해서는 그 클래스의 Class 객체를 얻어와야 한다.

@Test
public void getClassInfo() throws ClassNotFoundException {
    Class animalClass = Animal.class;
    assertEquals("Animal", animalClass.getSimpleName());

    Animal animal = new Dog("happy");
    animalClass = animal.getClass();
    assertEquals("Dog", animalClass.getSimpleName());

    animalClass = Class.forName("test.reflection.Animal");
    assertEquals("Animal", animalClass.getSimpleName());

    Class intClass = Integer.TYPE;
    assertEquals("int", intClass.getSimpleName());
}

특정 클래스의 Class 객체를 얻어오는 방법은 몇 가지가 있다.

  • 클래스이름.class
  • 인스턴스화 된 객체의 getClass() 메소드
  • Class.forName()

미리 클래스 이름에 접근할 수 있는 경우라면 클래스 이름 뒤에 .class 를 붙이면 된다. 객체를 가지고 있다면 객체에서 .getClass() 메소드를 호출하면 Class를 얻을 수 있다.

런타임까지 클래스의 이름을 알 수 없는 경우이거나 접근 제어자때문에 소스코드에서 클래스에 접근 할 수 없는 상황이라면 Class에 정의되어 있는 정적 메소드인 forName() 메소드를 이용해 클래스 정보를 얻어올 수 있다. 이 때, forName() 메소드의 인자로는 패키지 이름을 포함한 FQN(Fully Qualified Name)을 사용해야 한다.

기본형(Primitive Type)의 경우 Integer 같은 래퍼(Wrapper) 클래스에 정의된 TYPE을 사용하면 된다.

클래스 이름

Class 객체에서 클래스 이름은 다음과 같이 가져올 수 있다.

@Test
public void getClassName() {
    Object dog = new Dog("Happy");
    Class<?> clazz = dog.getClass();

    assertEquals("Dog", clazz.getSimpleName());
    assertEquals("test.reflection.Dog", clazz.getName());
    assertEquals("test.reflection.Dog", clazz.getCanonicalName());
}

클래스에서 가져올 수 있는 이름에는 몇 가지 종류가 있다.

  • name : Class.forName() 등으로 클래스를 동적으로 로드하는데 사용한 이름 (ClassLoader 내에서 유니크함)
  • canonical name : import 구문에서 사용한 이름
  • simple name : 클래스의 간단한 이름 (유니크하지 않음)
  • type name : 타입을 설명하는 유용한 문자열 (toString 같은?)

각 이름들의 정확한 의미는 자바 스펙 등의 문서를 찾아보자.

클래스의 접근제어 정보

자바의 클래스는 접근 제어자를 갖는다. public, private, package-private 등의 접근 제어자를 갖는데 리플렉션을 통해 이 정보도 얻어낼 수 있다.

@Test
public void getAccessModifier() {
    Class animalClass = Animal.class;
    int animalModifier = animalClass.getModifiers();

    Class dogClass = Dog.class;
    int dogModifier = dogClass.getModifiers();

    Class eatingClass = Eating.class;
    int eatingModifier = eatingClass.getModifiers();

    assertTrue(Modifier.isPublic(animalModifier));
    assertTrue(Modifier.isAbstract(animalModifier));
    assertTrue(Modifier.isPublic(dogModifier));
    assertTrue(Modifier.isInterface(eatingModifier));
}

Class의 getModifiers() 메소드를 이용하면 클래스의 접근 제어 정보를 가져올 수 있다. 이 메소드에서 리턴되는 int 값은 클래스가 가지고 있는 접근제어자 들을 Flag 형태로 설정한 값이다.

따라서 리턴되는 int 값을 통해 접근제어자 정보를 알아내려면 리턴 값에 플래그들을 비트연산 & 을 통해 확인해야한다. 편리하게도 Modifier 클래스에는 플래그 정보를 해석할 수 있는 static 메소드들이 제공된다. isPublic, isAbstract, isInterface 등의 메소드를 이용해 클래스의 접근 제어 정보를 확인할 수 있다.

클래스의 패키지 정보

클래스가 속해있는 패키지에 대한 정보도 얻어 낼 수 있다.

@Test
public void getPackageInfo() {
    Class dogClass = Dog.class;
    Package pkg = dogClass.getPackage();

    assertEquals("test.reflection", pkg.getName());
}

getPackage() 메소드를 호출하면 Package 객체를 얻을 수 있고, 이 안에 패키지에 대한 정보들이 담겨있다.

부모 클래스(Superclass) 정보

다른 클래스를 상속해 구현된 경우라면 부모 클래스 정보를 얻어올 수도 있다. 클래스의 getSuperclass() 메소드를 이용하면 된다.

@Test
public void getSuperClass() {
    Dog dog = new Dog("Happy");
    String str = "This is dog";

    Class dogClass = dog.getClass();
    Class stringClass = str.getClass();

    assertEquals("Animal", dogClass.getSuperclass().getSimpleName());
    assertEquals("Object", stringClass.getSuperclass().getSimpleName());
}

예제에서는 Dog 클래스가 Animal 클래스를 상속해서 구현했다. 문자열을 다루는 String 클래스의 Superclass는 Object 임을 알 수 있다.

인터페이스 구현 정보

비슷하게 구현한 인터페이스의 정보도 가져올 수 있다.

@Test
public void getInterface() {
    Class dogClass = Dog.class;
    Class[] dogInterfaces = dogClass.getInterfaces();

    assertEquals(1, dogInterfaces.length);
    assertEquals("Movable", dogInterfaces[0].getSimpleName());
}

하나의 클래스는 여러개의 인터페이스를 구현하고 있을 수 있다. 따라서 Class에서는 getInterfaces() 메소드를 이용해서 구현하고 있는 인터페이스의 Class 객체 배열을 얻어오게 된다.

흥미로운 점은 클래스의 부모 클래스가 구현한 인터페이스 정보는 들어있지 않다는 것이다. Dog 클래스의 부모 클래스인 Animal 클래스는 Eating 인터페이스를 구현하고 있다. 하지만 이를 상속한 Dog 클래스의 getInterfaces()에서는 Eating 인터페이스 정보가 들어있지않다.

다시말하면 getInterfaces() 메소드를 통해서는 클래스 선언부에서 implements 키워드를 이용해 명시적으로 구현한 인터페이스만 얻을 수 있다.

생성자

클래스를 인스턴스화하려면 생성자를 호출해야한다. 자바 리플렉션에서는 클래스의 생성자 정보를 담기 위해 Constructor 클래스를 제공한다.

Class clazz = Class.forName("test.reflection.Dog");

// 인자가 없는 생성자
Constructor constructor1 = clazz.getDeclaredConstructor();

// 인자가 있는 생성자
Constructor constructor2 = clazz.getDeclaredConstructor(String.class);

// 모든 생성자들을 배열로 가져오기
Constructor[] constructors1 = clazz.getDeclaredConstrcutors();

// Public 접근제어자를 갖고 있는 생성자들을 배열로 가져오기
Constructor[] constructors2 = clazz.getConstructors();

자바에서 클래스의 메소드들은 모두 다른 메소드 시그니처를 가지고 있다. 이는 생성자에서도 마찬가지로 적용된다. 따라서 여러 생성자들 중에 특정 생성자의 정보를 가져오고 싶으면 메소드 시그니처를 구성하는 인자들의 타입 정보를 getDeclaredConstructor() 메소드에 전달해주면 된다.

만약 찾고자하는 메소드 시그니처의 생성자가 없다면 NoSuchMethodException 예외가 발생한다.

객체 생성

리플렉션을 이용하면 단순히 클래스의 정보를 얻어오는 것뿐만 아니라 클래스의 객체를 생성할 수도 있다.

public void createNewInstance() {
    Class dogClass = Dog.class;
    Constructor constructor1 = dogClass.getConstructor();
    Constructor constructor1 = dogClass.getConstructor(String.class);
    
    Dog dog1 = (Dog) constructor1.newInstance();
		Dog dog2 = (Dog) constructor2.newInstance("Happy");
}

Constructor 객체를 이용해서 newInstance() 메소드를 호출하면 생성자가 호출되어 그 클래스의 객체가 생성된다. 이 때, Constructor의 인자로 넘겨줘야하는 값들을 newInstance()의 인자로 넘겨줘야한다.

가끔 Class.newInstance() 메소드를 호출해서 기본 생성자를 호출해 객체를 생성하는 경우가 있다. 하지만 Java 9부터는 Deprecated 된 방색으로 Constructor를 이용해서 객체를 생성하기를 권장한다.

필드

리플렉션을 이용해 클래스의 필드(멤버변수) 정보를 얻어 오는 것도 가능하다.

Dog dog = new Dog("happy");
Class clazz = dog.getClass();

// 모든 접근가능한 필드 가져오기
Field[] fields = clazz.getFields();

// 클래스에 정의되어 있는 모든 필드 가져오기
Field[] allFields = clazz.getDeclaredFields();

// 이름으로 필드 가져오기
Field ageField = clazz.getField("age");

getFields() 메소드는 접근 가능한 모든 public 필드들을 가져온다. 이 메소드는 해당 클래스의 public 필드와 부모 클래스의 public 필드들을 가져온다.

혹은 getField() 메소드에 필드 이름을 전달해서 특정 필드를 가져올 수도 있다.

클래스에 선언된 private 필드는 getDeclaredFields() 메소드 혹은 getDeclaredField(fieldName)를 이용해 얻어올 수 있다. 다만 부모 클래스에서 선언된 private 필드에는 접근할 수 없다.

만약 잘못된 이름이나 없는 이름을 찾으려고하면 NoSuchFieldException이 발생하게 된다.

필드의 타입 정보는 getType() 메소드를 이용할 수 있다.

Dog dog = new Dog("happy");
Class clazz = dog.getClass();

// 이름으로 필드 가져오기
Field ageField = clazz.getField("age");
ageField.setAccessible(true);

Class fieldClass = ageField.getType();

이제 필드의 값에 어떻게 접근하고, 어떻게 수정하는지 알아보자.

Dog dog = new Dog("happy");
Class clazz = dog.getClass();

// 이름으로 필드 가져오기
Field ageField = clazz.getField("age");
ageField.setAccessible(true);

// 필드 값 설정
ageField.set(dog, 24);

// 필드 값 접근
System.out.println("age : " + ageField.get(dog));

private 필드의 값을 가져오기 위해서 우선 setAccessible 메소드를 해당 필드 객체에서 호출해 true로 설정해야 한다. 필드 객체의 set() 메소드를 이용하면 필드에 값을 설정할 수 있다. 이 때, 필드에 설정할 값과 함께 인스턴스 객체도 함께 넘겨준다. 이러면 그 인스턴스 객체의 해당 필드의 값이 설정된다.

만약 public static 멤버인 경우에는 클래스의 인스턴스를 명시할 필요가 없다.

만약 Field 객체의 get() 메소드에 null 을 넘겨주면 그 필드의 기본 값 정보를 알아올 수 있다.

메소드

자바 리플렉션을 이용해 클래스에 정의되어 있는 메소드 정보를 가져올 수 있다. 또 한, 런타임에 동적으로 메소드를 호출 할 수도 있다.

Class clazz = Class.forName("test.Person");

// 이름으로 메소드 정보 가져오기
Method method1 = clazz.getDeclaredMethod("getAge");

// 이름과 인자 타입으로 메소드 정보 가져오기
Method method2 = clazz.getDeclaredMethod("setName", String.class);

// 인자가 없는 메소드 가져오기
Method method3 = clazz.getDeclaredMethod("methodName", null);

// 모든 메소드 가져오기
Method[] methods1 = clazz.getDeclaredMethods();

// Public 메소드 가져오기 (본 클래스와 부모 클래스)
Method[] methods2 = clazz.getMethods();

getMethods() 메소드를 호출하면 해당 클래스와 부모 클래스에 정의되어 있는 public 클래스들을 가져온다. 만약 해당 클래스에 있는 private 메소드 정보에도 접근하려면 getDeclaredMethod()를 호출하면 된다.

메소드 호출

이렇게 가져온 메소드는 동적으로 호출할 수 있다.

Class clazz = Class.forName("test.Person");
Person person = new Person("kim", 23);

// person 객체에서 method 호출
Method method = clazz.getMethod("getAge");
method.invoke(person);

Method 객체의 invoke() 메소드를 통해 동적으로 메소드를 호출 할 수 있다. 이 때, 인스턴스화 된 클래스의 객체를 함께 넘겨주면 그 객체에서 해당 메소드를 수행한 것처럼 동작하게 된다.

마치며

본 포스트에서 자바 리플렉션의 모든 정보를 다 커버하고 있지는 않다. 리플렉션을 이용해 동적으로 메소드를 호출하고, 객체의 필드를 변경하면 프로그래밍에 상당한 유연성을 가져올 수 있다.

하지만 리플렉션을 이용해서 메소드를 호출하고 필드를 수정하면 컴파일 타임이 아닌 런타임에 에러가 발생하기 때문에 생각하지 못한 버그가 런타임에 발생할 수 있다. 따라서 꼭 필요한 상황이 아니라면 리플렉션을 남용하지 않는 것이 좋다.

댓글