져니의 개발 정원 가꾸기

Reflection API 본문

개발노트/Spring | Java

Reflection API

전전쪄니 2023. 3. 1. 23:58

reflection API란?

출처 - GeeksforGeeks

  • 런타임에 메소드, 클래스, 인터페이스등을 검사하거나 조작하는데 쓰이는 api이다.
  • java.lang.Class와 java.lang.reflect 패키지와 관련있다.

특징

1. 객체가 속한 클래스에 대한 정보와 클래스의 실행가능한 메서드에 대한 정보를 제공한다. (클래스타입을 알지 못하는 객체라도 ok)

 리플렉션은 java.lang.Class 객체를 만드는 것에서 시작한다. Class 객체는 크게 두 가지 기능을 제공한다.

  •  런타임에 클래스의 메타데이터를 가져오는 메서드를 제공.
  • 클래스의 런타임 동작을 검사하고 변경하는 메서드를 제공.

실제 Class 클래스가 제공하는 메서드 몇가지를 살펴보자.

  메소드 상세
클래스 로드&가져오기 public static Class forName(String className)throws ClassNotFoundException
필드 목록 가져오기 public Field[] getDeclaredFields()throws SecurityException
생성자 목록 가져오기 public Constructor[] getDeclaredConstructors()throws SecurityException
메서드 목록 가져오기 public Method[] getDeclaredMethods()throws SecurityException
원시타입인지 확인 public boolean isPrimitive()
새로운 객체 생성 public Object newInstance()throws InstantiationException,IllegalAccessException

 

메소드들을 보면 반환된 리턴타입이 Field, Constructor, Method와 같은 것들이 있는데 이는 java.lang.reflect패키지에 있는 클래스들이다.

아래 문장은 java SE8 doc에 적혀진 java.lang.reflect 패키지에 대한 설명이다.

Provides classes and interfaces for obtaining reflective information about classes and objects

달리 말하면, 객체와 클래스를 반영하는 정보 를 얻는 클래스와 인터페이스를 제공한다는 말이다. 즉, Field, Constructor, Method와 같은 클래스는 객체와 타깃 클래스를 반영하는 정보(각각 필드, 생성자, 메서드)에 대한 클래스인 것이다.

 

 정리해보자면, 우리는 Class<T>를 통해 타킷 클래스의 필드, 메서드, 생성자 등과 같은 정보들을 가져올 수 있다, 이는 reflect패키지의 클래스들의 형태로 제공되고, 해당 클래스들의 메서드를 사용해 접근하고자 하는 대상 객체에 대해 다양한 정보들을 얻고 조작할 수 있다. (자세한 예는 아래 사용예시에서 보도록하자.)

2. 접근제한자에 상관없이 런타임에 메서드를 호출할 수 있다. (약간 마스터키의 느낌)

JVM이 실행될 때 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장되는데, 이 정보를 활용하여 클래스 이름으로 static 영역에서 정보를 가지고 온다. 그래서 런타임에도 가져올 수 있는 것이다.

접근제한자는 리플렉션api에서 제공하는 .setAccessible 과 같은 메서드를 통해서 조작이 가능하기 때문에 접근제한자가 private인 함수일지라도 호출가능하다.

사용예시

은행계좌인 Account 계좌에 대한 클래스를 생성하여 리플렉션을 사용하는 예제를 작성해보았다. 리플렉션 API에서 제공하는 Field, Constructor, Method 클래스를 이용하여 클래스의 필드, 생성자, 메서드들의 정보를 조회하거나 조작하는 예제이다. 예제를 보다보면 알겠지만 .setAccessible() 리플렉션의 메서드를 통해서 접근제한자도 변경하여 값을 조작할 수도 있다.
(리플렉션 관련하여 더 많은 메서드와 클래스들을 제공하고 있으니, 더 자세히 알고 싶다면 아래 링크들과 Java docs를 참고해보자.
- https://www.baeldung.com/java-reflection,
- https://www.oracle.com/technical-resources/articles/java/javareflection.html 

- https://docs.oracle.com/javase/tutorial/reflect/index.html )

 

package com.example.simple_practice.reflection;

public class Account {
    private String name = "anonymous";
    private long number;
    private long balance;

    public Account() {
    }

    public Account(String name, long number, long balance) {
        this.name = name;
        this.number = number;
        this.balance = balance;
    }

    public void deposit(long amount) {
        balance += amount;
    }

    public void withdraw(long amount) {
        balance -= amount;
    }

    public long getBalance() {
        return balance;
    }

    public String getName() {
        return name;
    }

    public long getNumber() {
        return number;
    }
}
class AccountTest {

    @Test
    @DisplayName("리플렉션으로 클래스의 생성자 정보를 가져오기")
    void inspectClassConstructor_by_reflection() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Constructor<?> constructor = object.getConstructor();
        Constructor<?> constructor2 = object.getConstructor(String.class, long.class, long.class);

        Account myAccount = (Account) constructor2.newInstance("gigi", 12345, 10000000);
        Account yourAccount = (Account) constructor.newInstance();

        assertThat(myAccount.getName()).isEqualTo("gigi");
        assertThat(yourAccount.getName()).isEqualTo("anonymous");
    }

    @Test
    @DisplayName("리플렉션으로 인자가 세 개인 생성자 정보를 가져오기")
    void inspectClassConstructorsParameters_by_reflection() throws ClassNotFoundException, NoSuchMethodException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Constructor<?>[] constructors = object.getConstructors();
        Constructor<?> constructor3 = object.getConstructor(String.class, long.class, long.class);

        Constructor<?> expectedConstructor = Arrays.stream(constructors)
                .filter(constructor -> constructor.getParameterTypes().length == 3)
                .findFirst()
                .orElseThrow(ClassNotFoundException::new);

        List<Class<?>> givenParams = List.of(constructor3.getParameterTypes());
        List<Class<?>> expectedParams = List.of(expectedConstructor.getParameterTypes());
        assertThat(expectedParams).hasSameElementsAs(givenParams);
    }

    @Test
    @DisplayName("리플렉션으로 클래스의 필드 정보 가져오기")
    void inspectClassFields_by_reflection() throws ClassNotFoundException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Field[] fields = object.getDeclaredFields();
        List<String> fieldNames = Arrays.stream(fields)
                .map(Field::getName)
                .toList();

        assertThat(fieldNames.size()).isEqualTo(3);
        assertTrue(fieldNames.containsAll(List.of("name", "number", "balance")));
    }

    @Test
    @DisplayName("리플렉션으로 private 필드 값 변경하기")
    void modifyClassFields_by_reflection() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Account modifyingAccount = (Account) object.getConstructor().newInstance();
        Field nameField = object.getDeclaredField("name");
        nameField.setAccessible(true); // private -> public
        nameField.set(modifyingAccount, "sally");

        assertThat(modifyingAccount.getName()).isEqualTo("sally");
    }

    @Test
    @DisplayName("리플렉션으로 메서드 정보 가져오기")
    void inspectClassMethods_by_reflection() throws ClassNotFoundException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Method[] methods = object.getDeclaredMethods();
        List<String> fieldNames = Arrays.stream(methods)
                .map(Method::getName)
                .toList();

        assertThat(fieldNames.size()).isEqualTo(5);
        assertTrue(fieldNames.containsAll(List.of("deposit", "withdraw", "getBalance", "getName", "getNumber")));
    }

    @Test
    @DisplayName("리플렉션으로 메서드 호출하기")
    void invokeClassMethods_by_reflection() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> object = Class.forName("com.example.simple_practice.reflection.Account");
        Account account = (Account) object.getConstructor().newInstance(); // default constructor
        Method depositMethod = object.getMethod("deposit", long.class);
        depositMethod.invoke(account, 10000);

        Method getBalanceMethod = object.getMethod("getBalance");
        long balance = (long) getBalanceMethod.invoke(account);

        assertThat(balance).isEqualTo(10000);
    }

    @Test
    @DisplayName("리플렉션으로 메서드 호출하기 - 구체 타입이 아닌 Object타입으로 생성해보기")
    void invokeClassMethods_by_reflection2_whenDontKnowConcreteClassType() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> accountClass = Class.forName("com.example.simple_practice.reflection.Account");
        Object object = new Account("", 123452, 20000); // compiler only knows object is Object type, not Account.
        Method depositMethod = accountClass.getMethod("deposit", long.class);
        Method withdrawMethod = accountClass.getMethod("withdraw", long.class);

        depositMethod.invoke(object, 10000);
        withdrawMethod.invoke(object, 3000);

        Method getBalanceMethod = accountClass.getMethod("getBalance");
        long balance = (long) getBalanceMethod.invoke(object);

        assertThat(balance).isEqualTo(27000);
    }
}

활용

우리가 애플리케이션을 개발할 떄는 리플렉션을 쓰는 일이 거~의 없다. 하지만 아래처럼 프레임워크나 라이브러리에서는 다양하게 쓰인다.

  • intelliJ 자동완성, 디버거, 테스트도구
  • JUnit, jackson 라이브러리(JSON -> POJO 역직렬화), Hibernate 라이브러리
  • Spring Container의 BeanFactory (런타임에 호출된 객체의 인스턴스를 동적으로 생성)
  • Spring Data JPA (setter가 없을 경우 reflection을 사용하여 기본생성자로 Entity생성)

장점

  • 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(ex. 메서드, 타입)에 접근할 수 있다.
    프레임워크나 라이브러리의 경우 클라이언트가 어떤 클래스를 만들지 예측하기 어렵다는 문제가 있는데, 리플렉션을 사용하면 런타임에 클래스 정보를 얻어와 이 문제를 해결할 수 있다. (그래서 리플렉션은 애플리케이션 개발보다는 프레임워크나 라이브러리에서 많이 사용된다.)

단점

  • 캡슐화 원칙이 깨질 수 있다. 
    리플렉션을 사용하면 클래스의 전용 메서드 및 필드에 접근할 수 있는데, 이는 중요한 데이터를 외부로 유출할 수 있다는 말이기도 하다. 즉, 보안이 취약하는 것이다. 가령 어떤 한 클래스가 있고 누군가가 이 클래스에 접근해 필드 하나를 null로 설정하면, 후에 다른 누군가가 이 필드와 관련된 행위를 수행할 때 nullPointerException이 발생할 수 있다.
  • 성능 오버헤드가 있다.
    아무래도 런타임에 동적으로 행동이 결정되다 보니 JVM 최적화가 이루어질 수 없어 느려진다.

마무리...

이번에 공부하면서 보니 리플렉션이 아래 그림과 같은 느낌..?

참고

https://tecoble.techcourse.co.kr/post/2020-07-16-reflection-api/

https://www.geeksforgeeks.org/reflection-in-java/

https://www.javatpoint.com/java-reflection

https://da-nyee.github.io/posts/woowacourse-why-the-default-constructor-is-needed/

https://bangu4.tistory.com/191