티스토리 뷰

Java

Java - 리플렉션

gyeonghyun 2024. 11. 25. 20:56

클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능을 리플렉션(Reflection) 이라 한다.

리플렉션을 통해 프로그램 실행 중에 클래스, 메서드, 필드 등에 대한 정보를 얻거나, 새로운 객체를 생성하고 메서드를 호출하며, 필드의 값을 읽고 쓸 수 있다.

 

 

리플렉션을 통해 얻을 수 있는 정보

  • 클래스의 메타데이터 : 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스 등
  • 필드 정보 : 필드의 이름, 타입, 접근 제어자를 확인하고, 해당 필드의 값을 읽거나 수정할 수 있다.
  • 메서드 정보 : 메서드 이름, 반환 타입, 매개변수 정보를 확인하고, 실행 중에 동적으로 메서드를 호출할 수 있다.
  • 생성자 정보 : 생성자의 매개변수 타입과 개수를 확인하고, 동적으로 객체를 생성할 수 있다.\

 

 

예제를 위한 기본 클래스

public class BasicData {
    public String publicField;
    private int privateField;

    public BasicData() {
        System.out.println("BasicData.BasicData");
    }

    private BasicData(String data) {
        System.out.println("BasicData.BasicData: " + data);
    }

    public void call() {
        System.out.println("BasicData.call");
    }

    public String hello(String str) {
        System.out.println("BasicData.hello");
        return str + " hello";
    }

    void defaultMethod() {
        System.out.println("BasicData.defaultMethod");
    }
    private void privateMethod() {
        System.out.println("BasicData.privateMethod");
    }

    protected void protectedMethod() {
        System.out.println("BasicData.protectedMethod");
    }
}

 

 

public class BasicV1 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 클래스 메타데이터 조회 방법 3가지

        // 1. 클래스에서 찾기
        Class<BasicData> basicDataClass1 = BasicData.class;
        System.out.println("basicDataClass1 = " + basicDataClass1);

        // 2. 인스턴스에서 찾기
        BasicData basicInstance = new BasicData();
        Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
        System.out.println("basicDataClass2 = " + basicDataClass2);

        // 3. 문자로 찾기
        String className = "reflection.data.BasicData"; //패키지명 주의
        Class<?> basicDataClass3 = Class.forName(className);
        System.out.println("basicDataClass3 = " + basicDataClass3);

    }
}

 

 

 

클래스의 메타데이터는 Class 라는 클래스로 표현된다. 그리고 Class라는 클래스를 흭득하는 3가지 방법이 있다.

 

 

 

1. 클래스에서 찾기

Class<BasicData> basicDataClass1 = BasicData.class;

 

클래스명에 .class를 사용하면 흭득할 수 있다.

 

2. 인스턴스에서 찾기

BasicData basicInstance = new BasicData();
Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();

 

인스턴스에서 .getClass() 메서드를 호출하면 흭득할 수 있다.

반환 타입을 보면 Class<? extends BasicData>로 표현되는데, 실제 인스턴스가 BasicData 타입일 수도 있지만, 그 자식 타입일 수 도 있기 때문이다.

 

Parent parent = new Child();
Class<? extends Parent> parentClass = parent.getClass();

 

Parent 타입을 통해 getClass()를 호출 했지만, 실제 인스턴스는 Child이다. 따라서 제네릭에서 자식 타입도 허용할 수 있도록 ? extends Parent를 사용한다.

 

3.문자로 찾기

String className = "reflection.data.BasicData"; //패키지명 주의
Class<?> basicDataClass3 = Class.forName(className);

 

단순히 문자로 클래스의 메타데이터를 조회할 수 있다. 예를 들어 콘솔에서 사용자 입력으로 원하는 클래스를 동적으로 찾을 수 있다는 뜻이다.

 

 

기본 정보 탐색

public class BasicV2 {
    public static void main(String[] args) {
        Class<BasicData> basicData = BasicData.class;

        System.out.println("basicData.getName() : " + basicData.getName());
        System.out.println("basicData.getSimpleName() : " + basicData.getSimpleName());
        System.out.println("basicData.getPackage() : " + basicData.getPackage());
        System.out.println("basicData.getSuperclass() : " + basicData.getSuperclass());
        System.out.println("basicData.getInterfaces() : " + Arrays.toString(basicData.getInterfaces()));
        System.out.println("basicData.getSuperclass() : " + basicData.isInterface());
        System.out.println("basicData.getSuperclass() : " + basicData.isEnum());
        System.out.println("basicData.getSuperclass() : " + basicData.isAnnotation());

        int modifiers = basicData.getModifiers();
        System.out.println("modifiers = " + modifiers);
        System.out.println("isPublic : " + Modifier.isPublic(modifiers));
        System.out.println("Modifier.toString  : " + Modifier.toString(modifiers));

    }
}

 

 

 

  • 클래스 이름, 패키지, 부모 클래스, 구현한 인터페이스, 수정자 정보등 다양한 정보를 흭득 할 수 있다.

 

참고로 수정자는 접근 제어자와 비 접근 제어자(기타 수정자)로 나눌 수 있다.

  • 접근 제어자 : public, protected, default, (package-private), private
  • 비 접근 제어자 : static, final, abstract, synchronized, volatile 등

getModifiers()를 통해 수정자가 조합된 숫자를 얻고, Modifier를 사용해서 실제 수정자 정보를 확인할 수 있다.

 

 

메서드 메타데이터

public class MethodV1 {
    public static void main(String[] args) {
        Class<BasicData> hellClass = BasicData.class;

        System.out.println("==== methods() ====");
        Method[] methods = hellClass.getMethods();
        for (Method method : methods) {
            System.out.println("method = " + method);
        }

        System.out.println("===== declaredMethods =====");
        Method[] declaredMethod = hellClass.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("method = " + method);
        }
    }
}

 

  • Class.getMethods() 또는 Class.getDeclaredMethods()를 호출하면 Method라는 메서드의 메타데이터를 얻을 수 있다. 이 클래스는 메서드의 모든 정보를 가지고 있다.

getMethods() vs getDeclaredMethods()

  • getMethods() : 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드를 반환
  • getDeclaredMethods() : 해당 클래스에서 선언된 모든 메서드를 반환하며, 접근 제어자에 관계없이 반환 상속된 메서드는 포함되지 않음

 

 

 

동적 메서드 호출

 

public class MethodV2 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 정적 메서드 호출 - 일반적인 메서드 호출
        BasicData helloInstance = new BasicData();
        helloInstance.call(); // 이부분은 코드를 변경하지 않은 이상 정적이다.

        // 동적 메서드 호출 - 리플렉션 사용
        Class<? extends BasicData> helloClass = helloInstance.getClass();
        String methodName = "hello";

        // 메서드 이름을 변수로 변경할 수 있다.
        Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
        Object returnValue = method1.invoke(helloInstance, "hi");
        System.out.println("returnValue = " + returnValue);

    }
}

 

일반적인 메서드 호출 - 정적

 

인스턴스의 참조를 통해 메서드를 호출하는 방식이 일반적인 메서드 호출 방식이다.

이 방식은 코드를 변경하지 않는 이상 call() 대신 다른 메서드로 변경하는 것이 불가능하다.

helloInstance.call(); // 이부분은 코드를 변경하지 않은 이상 정적이다.

호출하는 메서드가 이미 코드로 작성되어서 정적으로 변경할 수 없는 상태이다.

 

 

동적 메서드 호출 - 리플렉션 사용

String methodName = "hello";
// 메서드 이름을 변수로 변경할 수 있다.
Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
Object returnValue = method1.invoke(helloInstance, "hi");
System.out.println("returnValue = " + returnValue);

 

리플렉션을 사용하면 동적으로 메서드를 호출할 수 있다.

 

Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
  • 클래스 메타데이터가 제공하는 getMethod() 에 메서드 이름, 사용하는 매개변수의 타입을 전달하면 원하는 메서드를 찾을 수 있다.
  • 여기서는 hello라는 이름에 String 매개변수가 있는 hello(String) 메서드를 찾는다.
Object returnValue = method1.invoke(helloInstance, "hi");
  • Method.invoke() 메서드에 실행할 인스턴스와 인자를 전달하면, 해당 인스턴스에 있는 메서드를 실행할 수 있다.
  • 여기서는 BasicData helloInstance = new BaiscData() 인스턴스에 있는 hello(String) 메서드를 호출한다.

 

여기서 메서드를 찾을 때 helloClass.getMethod(methodName, String.class) 에서 methodName 부분이 String 변수로 되어 있는 것을 확인할 수 있다. methodName은 변수이므로 예를 들어 사용자 콘솔 입력을 통해서 얼마든지 호출할 methodName을 변경할 수 있다.

 

따라서 여기서 호출할 메서드 대상은 정적으로 코드에 정해진 것이아니라, 언제든지 동적으로 변경할 수 있다. 그래서 동적 메서드 호출이라 한다.

 

 

 

예시

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int minus(int a, int b) {
        return a - b;
    }
}

 

 

public class MethodV3 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Scanner scanner = new Scanner(System.in);
        System.out.println("호출 메서드 : ");

        String methodName = scanner.nextLine();

        System.out.print("숫자 1 : ");
        int num1 = scanner.nextInt();
        System.out.print("숫자 2 : ");
        int num2 = scanner.nextInt();

        Calculator calculator = new Calculator();
        // 호출할 메서드를 변수 이름으로 동적으로 선택

        Class<? extends Calculator> aClass = calculator.getClass();
        Method method = aClass.getMethod(methodName, int.class, int.class);

        Object returnValue = method.invoke(calculator, num1, num2);
        System.out.println("returnValue = " + returnValue);

    }
}

 

 

 

 

필드 탐색과 값 변경

public class FieldV1 {
    public static void main(String[] args) {
        Class<BasicData> helloClass = BasicData.class;

        System.out.println("==== fields() ====");
        Field[] fields = helloClass.getFields();
        for (Field field : fields) {
            System.out.println("field = " + field);
        }

        System.out.println("==== declaredFields() ====");
        Field[] declaredField = helloClass.getDeclaredFields();
        for (Field field : declaredField) {
            System.out.println("field = " + field);
        }
    }
}

 

 

 

fields() vs declaredFields()

앞서 설명한 getMethods() vs getDeclaredMethods() 와 같다.

  • fields() : 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환
  • declaredFields() : 해당 클래스에서 선언된 모든 필드를 반환하며, 접근 제어자에 관계없이 반환, 상속된 필드는 포함되지않음

 

예제

public class User {
    private String id;
    private String name;
    private Integer age;

    public User(String id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

 

 

public class FieldV2 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

        User user = new User("id1", "userA", 20);
        System.out.println("기존 이름 : " + user.getName());

        Class<? extends User> aClass = user.getClass();
        Field nameField = aClass.getDeclaredField("name");

        // private 필드에 접근 허용, private 메서드도 이렇게 호출 가능
        nameField.setAccessible(true);
        nameField.set(user, "userB");
        System.out.println("변경된 이름 : " + user.getName());
        
    }
}

 

Field nameField = aClass.getDeclaredField("name");
  • name 이라는 필드를 조회한다.
  • 그런데 name 필드는 private 접근 제어자를 사용한다. 따라서 직접 접근해서 값을 변경하는 것이 불가능하다.
nameField.setAccessible(true);

 

  • 리플렉션은 private 필드에 접근할 수 있는 특별한 기능을 제공한다.
  • 참고로 setAccessible(true) 기능은 Method도 제공한다. 따라서 private 메서드를 호출할 수 있다.
nameField.set(user, "userB");

 

  • user 인스턴스에 있는 nameField의 값을 userB로 변경한다.

 

'Java' 카테고리의 다른 글

Java - ThreadLocal  (0) 2025.01.13
Java - 애노테이션  (0) 2024.11.26
Java - HTTP 서버 만들기 2  (0) 2024.11.25
Java - URL 인코딩  (0) 2024.11.25
Java - HTTP 서버 만들기 1  (1) 2024.11.18
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함