Java

리플렉션(Reflection)

devgenie 2024. 3. 14. 22:32

리플렉션이란?

Java의 리플렉션(Reflection) 기능은 런타임 시 클래스 정보에 접근하여, 그 클래스의 인스턴스를 생성하거나 메서드를 호출하는 등의 동작을 가능하게 해주는 기능을 제공한다. java.lang.reflect 패키지를 통해 사용할 수 있으며, 리플렉션을 사용해서 Java 코드 내에서 동적으로 객체의 속성에 접근하거나 수정할 수 있다. 뿐만아니라 클래스의 정보를 조회하거나 새로운 객체를 생성하는 작업들을 제공한다.

 

리플렉션은 힙 영역(Heap) 에 로드된 Class 타입의 객체를 통해 원하는 클래스의 정보에 접근한다. 힙 영역은 객체와 배열이 동적으로 할당되는 곳이다. Class 타입의 객체는 Java가 실행되고 JVM이 클래스를 로드하는 순간 생성되고 클래스에 대한 메타데이터를 포함하고 있는데, 이러한 구조이기 때문에 리플렉션 API를 통해 Class 객체에 다양한 조작이 가능하다.

JVM 내부 구조를 간략하게 나타낸 이미지 (출처 https://wikidocs.net/102803)

 

예를 들면 리플렉션을 이용하여 public, private 등과 같은 접근 제어자에 상관없이 클래스의 필드, 메서드, 생성자 등에 접근할 수 있다. 자바 웹 개발시 사용하는 Spring Framework에서는 DI, Annotation 기반의 설정 등의 기능을 제공하는데, 이러한 기능들의 구현에 리플렉션이 핵심적은 역할을 한다.

 

리플렉션 사용

1. Class.forName("클래스명")

문자열과 같은 클래스명에 해당되는 Class 객체를 반환한다.

try {
    Class<?> clazz = Class.forName("java.util.ArrayList");
    System.out.println(clazz.getName()); // 출력: java.util.ArrayList
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

 

클래스를 유일하게 식별할 수 있는 전체 경로(java.util.ArrayList)를 이용하여 ArrayList의 Class 객체(clazz)를 반환한 코드이다. 입력한 경로의 클래스가 존재하지 않는다면 'ClassNotFoundException' 이 발생된다

 

2. Class.newInstance()

클래스의 기본 생성자를 사용하여 인스턴스를 반환한다

try {
    Class<?> clazz = Class.forName("java.util.ArrayList");
    Object instance = clazz.getDeclaredConstructor().newInstance();
    System.out.println(instance instanceof ArrayList); // 출력: true
} catch (Exception e) {
    e.printStackTrace();
}

 

3. 메서드 접근 및 호출

getMethods()를 통하여 주어진 객체의 puiblic 메서드를 호출할 수 있다.
getDeclaredMethods()를 사용한다면 객체의 private메서드까지 전부 호출할 수 있다.

public class ReflectionTest {
    public static void main(String[] args) {
        String test = "TEST STRING";
        Class<? extends String> testClass = test.getClass();
        Arrays.stream(testClass.getMethods()) //.getDeclaredMethods()를 사용하면 private 메서드까지 호출한다
                .forEach(method -> {
                    System.out.println("method.getName : " + method.getName());
                    System.out.println(method);
                    System.out.println("-----------------------------------");
                });
    }
}

 

출력 결과

method.getName : equals
public boolean java.lang.String.equals(java.lang.Object)
-----------------------------------
method.getName : length
public int java.lang.String.length()
-----------------------------------
method.getName : toString
public java.lang.String java.lang.String.toString()
-----------------------------------
method.getName : hashCode
public int java.lang.String.hashCode()
-----------------------------------
method.getName : getChars
public void java.lang.String.getChars(int,int,char[],int)
-----------------------------------
method.getName : compareTo
public int java.lang.String.compareTo(java.lang.String)
-----------------------------------
기타 생략 ...

 

 

출력 결과를 보면 알 수 있듯이 getMethods()를 이용하여 객체의 메서드 명과 메서드를 호출할 수 있다. 

 

 

4. 필드 접근 및 수정

제공되는 다양한 방법으로 해당 객체의 필드에 접근 및 수정이 가능하다

  • Class.getDeclaredField(String name): 지정된 이름에 해당하는 필드를 Field 객체로 반환한다.
  • Field.setAccessible(boolean flag): 필드의 접근성을 설정. true로 설정하면 private 필드에도 접근할 수 있다.
  • Field.get(Object obj): 주어진 객체에서 해당 필드의 값을 가져온다.
  • Field.set(Object obj, Object value): 주어진 객체의 해당 필드에 값을 설정한다.
public class ReflectionTest {
    public static void main(String[] args) {
        try {
            SampleObject obj = new SampleObject();
            Field field = SampleObject.class.getDeclaredField("name");
            field.setAccessible(true); // private 필드에 접근 가능하도록 설정
            field.set(obj, "Modified Name"); // 필드 값 수정
            String value = (String) field.get(obj); // 필드 값 가져오기
            System.out.println(value); // 수정된 필드 출력: Modified Name
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

public class SampleObject {
    private String name = "Initial Name";
}

 

 

리플렉션을 이용하여 클래스가 특정 제약을 충족하는지 동적으로 체크

리플렉션을 사용하면 클래스가 특정 조건을 충족하는지 동적으로 검사할 수 있다.

예를들면 클래스에 정의된 필드의 수를 검사하거나 특정한 접근제한자를 가진 메서드의 존재 여부를 확인하는 것과 같은 검증 작업을 할 수 있다. 그리고 이러한 검증들에 실패할 경우 예외를 발생시킨다거나 다른 형태로 처리하는식의 코드 작성이 가능하다.

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class ReflectionRestrictionTest {

    public static void checkClassRestrictions(Class<?> clazz) throws Exception {
        // 인스턴스 필드 개수 제한 검사
        Field[] fields = clazz.getDeclaredFields();
        long instanceFieldCount = 0;
        for (Field field : fields) {
            if (!Modifier.isStatic(field.getModifiers())) {
                instanceFieldCount++;
            }
        }
        if (instanceFieldCount > 2) { // 예를 들어, 인스턴스 필드가 2개를 초과하는 것을 제한
            throw new Exception("클래스 " + clazz.getName() + "는 인스턴스 필드를 2개 이하로 가져야 합니다.");
        }

        // static 메서드의 존재 금지 검사
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            if (Modifier.isStatic(method.getModifiers())) {
                throw new Exception("클래스 " + clazz.getName() + "에 static 메서드가 존재해서는 안 됩니다.");
            }
        }
    }

    public static void main(String[] args) {
        try {
            checkClassRestrictions(MyClass.class);
            System.out.println("클래스가 제약 조건을 충족합니다.");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    static class MyClass {
        private int field1;
        private int field2;
        private int field3; // 이 필드 때문에 인스턴스 필드 개수 제한 조건에 위배됨

        public static void staticMethod() { // 이 메서드 때문에 static 메서드 존재 금지 조건에 위배됨
        }
    }
}

 

위 코드를 실행시키면 첫 분기문의 조건인 'instanceFieldCount > 2' 를 만족하게 되어 인스턴스의 필드 갯수를 초과한다는 에러를 출력하게 된다.

그리고 MyClass내의 필드를 2개 이하로 줄인다면 그 뒤의 조건인 'Modifier.isStatic(method.getModifiers())' 를 만족하게 되어 static 메서드가 존재하면 안된다는 에러를 출력하게 된다. 이와같이 리플렉션을 이용하면 인스턴스의 속성에 접근할 수 있다.

 

ChatGPT4 DALL.E : 자바 리플렉션을 상징하는 이미지 그려줘