[Java] 리플렉션(Reflection)
본문 바로가기
Java

[Java] 리플렉션(Reflection)

by IYK2h 2023. 6. 30.
728x90

리플렉션(Reflection)

리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API를 말한다. 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이라 할 수 있다.

 

어떤 경우에 사용되나?

런타임에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우

동적으로 객체를 생성하고 메서드를 호출하는 방법

자바의 리플렉션은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경하거나 메소드를 호출할 수 있다.

동적으로 클래스를 만들어서 의존 관계를 맺어줄 수 있다.

Spring의 Bean Factory를 보면, @Controller, @Service, @Repository 등의 어노테이션만 붙이면 Bean Factory에서 알아서 해당 어노테이션이 붙은 클래스를 생성하고 관리해 주는 것을 알 수 있다. 런타임에 해당 어노테이션이 붙은 클래스를 탐색하고 발견한다면, 리플렉션을 통해 해당 클래스의 인스턴스를 생성하고 필요한 필드를 주입하여 Bean Factory에 저장하는 식으로 사용된다.

 

사용 예시

동적인 바인딩을 이용한 기능을 제공 intelliJ의 자동완성 기능 스프링의 어노테이션이 리플렉션을 이용한 기능

 

클래스 정보 조회

실습 코드

package me.study;
​
import java.lang.annotation.Target;
​
@MyAnnotation
public class Book {
    private String A = "A";
    private static String B = "B";
    private static final String C = "C";
    public String D = "D";
    @MyAnnotation
    protected String E = "E";
​
//    @MyAnnotation
    public Book() {
​
    }
​
    public Book(String a, String d, String e) {
        A = a;
        D = d;
        E = e;
    }
​
    private void f() {
        System.out.println("F");
    }
​
    public void g() {
        System.out.println("G");
    }
​
    public int h(int i) {
        return 100;
    }
​
    static public int sum(int a, int b) {
        return a + b;
    }
}
package me.study;
​
import ... 
​
public class App {
​
    public static void main(String[] args)
            throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException, IllegalAccessException {
​
        // Class<T>에 접근하는 방법 3가지
        // 모든 클래스를 로딩 한 다음 Class<T>의 인스턴스가 생긴다.
​
        // 1. "타입.class"로 접근할 수 있다.
        Class<Book> bookClass = Book.class;
        // 2. "인스턴스.getClass()"로 접근할 수 있다.
        Book book = new Book();
        Class<? extends Book> bookInstenceClass = book.getClass();
        // 3. 문자열(FQCN:Fully Qualified Class Name)로 접근할 수 있다.
        Class<?> FQCNGetClass = Class.forName("me.study.Book");
​
        // 필드 접근
        Arrays.stream(FQCNGetClass.getFields()).forEach(System.out::println);
        /*
          출력 : public java.lang.String me.study.Book.D
          D 만 출력된 이유는 public 이기 때문
         */
​
        System.out.println();
​
        Arrays.stream(FQCNGetClass.getDeclaredFields()).forEach(System.out::println);
        /*
          getDeclaredFields() 을 사용한 모든 필드 가져오기
          public java.lang.String me.study.Book.D
          private java.lang.String me.study.Book.A
          private static java.lang.String me.study.Book.B
          private static final java.lang.String me.study.Book.C
          public java.lang.String me.study.Book.D
          protected java.lang.String me.study.Book.E
         */
​
        System.out.println();
​
        /*
          필드의 값을 가져오고 싶다.
          값을 가져오려면 인스턴스가 필요하다.
         */
​
        Arrays.stream(bookClass.getDeclaredFields()).forEach(f -> {
            try {
                f.setAccessible(true);
                // f.setAccessible(false); IllegalAccessException
                System.out.printf("%s %s\n", f, f.get(book));
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
        /*
          setAccessible 을 true 로 하지 않으면 접근이 불가능하다.
          setAccessible 의 default 값은 false 이다.
          private java.lang.String me.study.Book.A A
          private static java.lang.String me.study.Book.B B
          private static final java.lang.String me.study.Book.C C
          public java.lang.String me.study.Book.D D
          protected java.lang.String me.study.Book.E E
         */
​
        System.out.println();
​
        // 생성자 가져오기 getConstructors()
        Arrays.stream(bookClass.getConstructors()).forEach(System.out::println);
        /*
          public me.study.Book()
          public me.study.Book(java.lang.String,java.lang.String,java.lang.String)
         */
​
        System.out.println();
​
        // 부모 클래스 가져오기
        System.out.println(bookClass.getSuperclass());
        System.out.println(MyBook.class.getSuperclass());
​
        /*
          class java.lang.Object
          class me.study.Book
         */
​
        System.out.println();
        // 메서드 찾기
        Arrays.stream(bookClass.getMethods()).forEach(System.out::println);
​
        // 파라미터 없는 경우
        Method method1 = bookClass.getDeclaredMethod("g");
        /*
          혹은 null 입력
          Method method1 = bookClass.getDeclaredMethod("g", null);
          public void me.study.Book.g()
         */
​
        // 파라미터 있는경우
        Method method2 = bookClass.getDeclaredMethod("h", int.class);
        //public void me.study.Book.g()
​
        // 파라미터가 두 개 이상인 경우
        Method method3 = bookClass.getDeclaredMethod("sum", int.class, int.class);
        //public int me.study.Book.sum(int,int)
​
        Class[] parameterGroups = new Class[2];
        parameterGroups[0] = int.class;
        parameterGroups[1] = int.class;
        Method method4 = bookClass.getDeclaredMethod("sum", parameterGroups);
        //public int me.study.Book.sum(int,int)
​
        // Static 메서드 호출 또는 필드 변경
        Method method5 = bookClass.getDeclaredMethod("sum", int.class, int.class);
        System.out.println(method5.invoke(null, 10, 10));
        // 20
​
        // Static 필드
        Field field = bookClass.getDeclaredField("B");
        field.setAccessible(true);
        System.out.println(field.get(null));
        field.set(null, "It is new B");
        System.out.println(field.get(null));
        /*
          B
          It is new B
          */
​
        // static final 필드 접근
        Field field2 = bookClass.getDeclaredField("C");
        field2.setAccessible(true);
        System.out.println(field2.get(null));
        field2.set(null, "It is new C");
        //IllegalAccessException 발생 Can not set static final
      
        // method 수정 및 실행
        Method c = bookClass.getDeclaredMethod("g");
        c.invoke(book);
        // 출력 : G
    }
}

 

이미 초기화된 final 값을 변경하는 방법은 없나? 있다

public class Main {
  public static void main(String[] args) throws Exception {
    MyClass myObject = new MyClass();
    System.out.println(myObject.getValue()); // Output: 42
​
    Field field = myObject.getClass().getDeclaredField("value");
    field.setAccessible(true);
​
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
​
    field.setInt(myObject, 24);
​
    System.out.println(myObject.getValue()); // Output: 24
  }
}
​
class MyClass {
  private final int value = 42;
​
  public int getValue() {
    return value;
  }
}

이 예제에서는 반사를 사용하여 MyClass 클래스의 값 필드에 액세스하고 액세스 가능성을 수정하며 Field.setInt(field, field.getModifier() & ~Modifier라는 수식어를 사용하여 최종 수식어를 제거합니다.FINAL) 라인. 그런 다음 field.setInt(myObject, 24)를 사용하여 필드에 새 값을 설정할 수 있습니다.

반영을 사용하여 최종 필드를 수정하는 것은 일반적으로 잘못된 관행으로 간주되며 이러한 동작에 대한 강력한 정당성이 있는 예외적인 경우에만 수행해야 합니다. 코드의 최종 필드를 수정하기 전에 잠재적인 위험과 결과를 이해하는 것이 중요합니다.

어노테이션과 리플렉션

어노테이션은 주석과 비슷하지만 더 하는일이 많다.

어노테이션 생성

public @interface MyAnnotation {
}

어노테이션 사용

@MyAnnotation
public class Book {
    private String name;
​
}

애노테이션의 Retention

소스 코드와, 클래스 파일(바이트 코드)에는 남지만 클래스 파일(바이트 코드)을 로딩했을 때 메모리에는 남지 않는다. 만약, 메모리에 남기고 싶다면 @Retention(RetentionPolicy.RUNTIME) 을 사용하면된다.

→ Retention 의 Default 는 CLASS

Book 클래스 파일(바이트 코드)에 어노테이션 정보까지 보려면 javap -c -v ~~/Book.class

Class 의 getAnnotations()

import java.util.Arrays;
​
public class App {
    public static void main(String[] args){
        Arrays.stream(Book.class.getAnnotations()).forEach(System.out::println);
    }
}
// @Retention을 설정하지 않았다면 아무 값도 나오지 않는다.
// 
// @Retention(RetentionPolicy.RUNTIME)
// @me.study.MyAnnotation()

Annotation Target : 선언 위치 정의

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface MyAnnotation {
}

Target을 Type과 Field로 지정해두고 생성자에서 사용하면 컴파일 에러 발생한다.

어노테이션은 재한된 값을 가질 수 있다.

  • Primitive Type
  • Primitive Type의 Reference Type (String, Integer, ...)
  • Default 값 또한 가질 수 있다.
    • default 키워드
    • 기본값을 주지 않으면 Compile Error가 발생한다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface MyAnnotation {
    String name() default "name";
​
    int number() default 1000;
}

단일 값을 받는 상황이라면 value를 이용하여 간결하게 사용할 수 있다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface MyAnnotation {
    String value();
}
@MyAnnotation("hi")
public class Book {
    @MyAnnotation("hello")
    private String name;
}

자식 클래스에서 본인 애노테이션과 부모의 애노테이션을 가져오는 방법

inherited : 상속이 되는 애노테이션 임을 명시

import java.lang.annotation.*;
​
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Inherited
public @interface MyAnnotation {
    String value();
}
@MyAnnotation("hi")
public class Book {
​
    @MyAnnotation("hello")
    private String name;
​
}
public class MyBook extends Book {
}

MyBook 클래스에서 getAnnotations();

import java.util.Arrays;
​
public class App {
    public static void main(String[] args){
        Arrays.stream(MyBook.class.getAnnotations()).forEach(System.out::println);
    }
}
//출력 : @me.study.MyAnnotation(value=hi)

클래스의 getDeclaredAnnotations();

클래스에 직접 선언된 어노테이션만 가져오고 싶다. 상속된 어노테이션 X

import java.util.Arrays;
​
public class App {
    public static void main(String[] args){
      Arrays.stream(MyBook.class.getDeclaredAnnotations()).forEach(System.out::println);
    }
}
//출력 : 

리플랙션을 이용해 어노테이션들 값 변경

import java.util.Arrays;
​
public class App {
    public static void main(String[] args){
        // Fields 에 있는 애노테이션들!
        Arrays.stream(Book.class.getDeclaredFields()).forEach(f -> {
            Arrays.stream(f.getAnnotations()).forEach(a -> {
                if(a instanceof MyAnnotation){
                    MyAnnotation myAnnotation = (MyAnnotation) a;
                    System.out.println(myAnnotation.value());
                }
            });
        });
    }
}
728x90

'Java' 카테고리의 다른 글

[Java] I/O  (0) 2023.07.22
[Java] 어노테이션(Annotation)  (0) 2023.07.04
[Java] 바이트코드 조작  (0) 2023.06.23
[Java] 코드 커버리지  (0) 2023.06.21
[Java] Enum  (2) 2023.06.14

댓글