[Java] 리플렉션(Reflection)공부하기

리플렉션(Reflection)이란?


리플렉션은 런타임 시 클래스, 객체, 메서드, 필드의 동작을 검사하고 조작할 수 있게 해주는 자바의 기능 중 하나이다. 그리고 표준 자바 코드만으로는 할 수 없는 클래스를 동적으로 로드, 새로운 객체 생성, 메서드 호출 같은 작업을 수행할 수 있다.

 

리플렉션에 대한 정의만 봐서는 와닿지도 않고 이해하기 쉽지 않다.

 

간단히 리플렉션이 할 수 있는 기능을 예시 코드를 보면서 이해해 보자.

 

우선 앞으로 다른 예제 코드에서 계속 사용할 Person 클래스를 만들고 시작하자.

person 클래스에는 private로 생성된 name, age 필드가 있고, 생성자와 getter가 존재한다. 

 

Person

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

 

먼저 필드값을 가져오는 메서드를 알아보자.

 

getDeclaredFields

public class Reflection {
    public static void main(String[] args) throws IllegalAccessException {
        Person person = new Person("Jun", 25);
        printPersonDetails(person);
    }

    public static void printPersonDetails(Person person) throws IllegalAccessException {
        Field[] fields = Person.class.getDeclaredFields();

        for (Field field : fields) {
            field.setAccessible(true);
            System.out.println(field.getName() + ": " + field.get(person));
        }
    }
}

위의 코드에서 Person 객체를 파라미터로 전달받은 printPersonDetails() 메서드를 생성한다. 이후 리플렉션을 사용해 Person 객체 안에 존재하는 필드 값(이름과 나이)을 출력하기 위함이다. 

 

printPersonDetails() 메서드를 보면 첫 줄에 Person.class.getDeclaredFields(); 가 보인다 메서드의 이름만 봐도 선언된 필드의 값을 가져오는 것을 확인할 수 있다.

이후 가져온 필드 값이 private로 되어있기 때문에 반복문을 이용해 setAccessible() 메서드를 사용해 true로 변경해 접근할 수 있도록 설정해 준다.

 

마지막으로 가져온 field 객체에서 getName() 메서드를 사용해 필드 이름과 get() 메서드를 사용해 해당 필드의 값을 가져와 같이 출력해 준다.

 

결과는 아래와 같다. 

 

 

 

getMethod

 

public class Reflection {
    public static void main(String[] args) {
        Method[] methods = Person.class.getMethods();
        for (Method method : methods) {
            System.out.println("method = " + method.getName());
        }
    }
}
위 메서드는 person 객체안에 존재하는 모든 메서드를 가져오는 메서드이다. 
출력해보면 person 객체 안에 직접 선언해준 메서드 말고도 많은 메서드들이 출력이 된다.

 

 

 

컴파일 시점에 클래스의 이름을 모르더라도 런타임 시점에 클래스를 인스턴스화할 수 있다.


다음 알아볼 리플렉션의 기능은 컴파일 시점에 클래스의 이름을 모르더라도 런타임 시점에 클래스의 객체를 생성할 수 있도록 하는 기능이다. 즉 코드를 작성하는 시점에는 생성할 클래스의 이름을 몰라도 되고 런타임 시점에 사용자의 입력이나 서비스에 따라 결정이 된다.

 

아래 코드를 보면서 이해해 보자.

public class Reflection {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter the name of the class to instantiate:");
        String className = scanner.nextLine();

        try {
            Class<?> newClass = Class.forName(className);
            Constructor<?> constructor = newClass.getDeclaredConstructor();
            Object object = constructor.newInstance();
            System.out.println("Created object of class: " + object.getClass().getName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

try 안의 구문을 보면 생성할 클래스의 이름은 정해져 있지 않고 사용자가 Scanner로 입력한 클래스 이름으로 정해지는 것으로 확인할 수 있다. 즉 생성할 클래스의 이름은 컴파일 시점에는 몰라도 되기 때문에 유연하게 코드를 작성할 수 있다.

 

입력받은 클래스의 생성자를 getDeclaredConstructor() 메서드로 해당 클래스의 생성자를 가져온다.

이후 가져온 생성자를 이용해 새로운 객체를 만든다. (newInstance() 메서드)

 

실제로 위에서 만든 Person 클래스를 입력해 보면 아래와 같은 결과를 출력한다.

 

 

컴파일 시점에 클래스의 필드와 메서드의 이름을 모르더라도 런타임 시점에 접근하고 수정할 수 있다.


다음은 컴파일 시점에 필드 이름을 모르더라도 객체의 필드 값에 접근하고 수정하는 기능을 알아보자.

 

이름이 "John"이고 나이가 24인 사람 객체를 생성하고 "John" 이름을 변경하는 코드를 보자.

public class Reflection {
    public static void main(String[] args) {
    	Person person = new Person("John", 24);
        
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter the name of the field to access and modify:");
        String fieldName = scanner.nextLine();

        try {
            Field field = Person.class.getDeclaredField(fieldName);
            field.setAccessible(true);
            Object fieldValue = field.get(person);
            System.out.println("Current value of field " + fieldName + ": " + fieldValue);

            System.out.println("Enter the new value for the field:");
            Object newValue = scanner.nextLine();
            field.set(person, newValue);

            System.out.println("New value of field " + fieldName + ": " + field.get(person));

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

먼저 Person 객체를 생성하고 수정할 필드값을 입력받도록 해 컴파일 시점에는 person 객체의 수정할 필드의 이름을 몰라도 된다.

 

Person 객체의 필드를 getDeclaredField() 메서드를 이용해 사용자가 입력한 필드(name 입력) 객체를 얻고 setAccessible() 메서드를 이용해 얻어온 필드에 접근할 수 있도록 true로 변경해 준다. person 클래스의 필드 접근제한자를 private로 선언해 주었기 때문이다. 

 

이후 얻어온 필드의 수정할 값을 입력해 준다. (Jun 입력)

입력한 값을 필드 객체의 set() 메서드를 이용해 John을 Jun으로 변경해 준다.

 

출력된 결과는 아래와 같다

 

 

컴파일 시점에 필드 이름을 모르더라도 런타임 시점에 객체의 필드 값에 동적으로 접근하고 수정할 수 있다. 

 

리플렉션의 장점


1. 클래스의 동적 인스턴스화

리플렉션을 사용하면 컴파일 시점에 클래스의 이름을 모르더라도 런타임 시점에 클래스를 인스턴스화할 수 있다. 즉, 런타임 시점에 코드를 조작할 수 있어 동적(사용자의 입력을 기반)으로 작동하는 코드를 생성해야 하는 상황에서 코드를 유연하게 작성할 수 있는 장점이 존재한다.

 

2. private 또는 protected 된 필드 및 메서드에 대한 접근

리플렉션을 사용하면 일반적인 Java 코드로는 할 수 없는 private, protected 필드와 메서드에 접근하고 수정할 수 있다. 이는 제어할 수 없는 클래스의 동작을 수정할 수 있다는 장점이 있다.

 

하지만 이는 단점이 되기도 한다. private로 외부 접근을 제한했는데 setAccessible(true) 메서드로 접근할 수 있도록 조작할 수 있기 때문에 조심히 사용해야 한다.

 

 

정리


정리해보면 리플렉션을 사용하면 동적이고 변화하는 환경에 적응할 수 있는 코드를 작성할 수 있다. 하지만 리플렉션을 사용하면 복잡해져 디버그하기 어려울 수 있다는 단점도 존재하는것 같다. 
 
리플렉션에 대해서는 위에서 살펴본 메서드 말고도 많은 메서드도 존재하는데 하나씩 사용해보면서 공부하는게 리플렉션에 대해 이해하는데 훨씬 도움이 된다고 생각한다.

그리고 리플렉션은 우리가 많이 사용하는 Spring의 DI, JPA 와도 관계가 있으니 다음에 한번 공부해 봐야겠다.