본문 바로가기
JAVA

[JAVA] 리플렉션 (Reflection)

by yonikim 2023. 12. 22.
728x90

리플렉션 (Reflection) 이란? 


 

리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있는 자바 API 를 말한다. 

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

 

리플렉션은 언제 사용할까?

  • 동적으로 클래스를 사용해야 하는 경우
  • 코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우
  • 프레임워크나 IDE 에서 이런 동적 바인딩을 이용한 기능을 제공한다. IntelliJ 의 자동완성 기능, 스프링의 어노테이션이 리플렉션을 이용한 기응이다.

리플렉션을 사용하여 가져올 수 있는 정보는 다음과 같다. 

  • Class
  • Constructor
  • Method
  • Field

 

JVM 은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장한다.

그렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, 리플렉션이라는 이름을 가지게 되었다.

리플렉션을 사용하면 생성자, 메소드, 필드 등 클래스에 대한 정보를 아주 자세히 알아낼 수 있다.

 

대표적으로 여러 라이브러리, 프레임워크에서 사용되는 어노테이션이 리플렉션을 사용한 예시이다. 

어노테이션은 그 자체로는 아무 역할도 하지 않는다. 리플렉션 덕분에 우리가 스프링에서 @Component, @Bean 과 같은 어노테이션을 프레임워크의 기능을 사용하기 위해 사용할 수 있는 것이다.

 

또한, 인텔리제이와 같은 IDE 에서 Getter, Setter 를 자동으로 생성해주는 기능도 리플렉션을 사용하여 필드 정보를 가져와 구현한다고 한다. 이와 같이 리플렉션은 다양한 곳에서 무궁무진한 방법으로 사용될 수 있다. 

 

리플렉션을 사용하면 접근 제어자와 무관하게 클래스의 필드나 메소드도 가져와서 호출할 수 있다는 점이다.

 

 

Class 


리플렉션의 가장 핵심은 Class 이다. Class 는 java.lang 패키지에서 제공된다. 어떻게 특정 클래스의 Class 인스턴스를 획득할 수 있을까?

 

Class 객체 획득 방법 

Class<Member> aClass = Member.class; // (1)

Member member1 = new Member();
Class<? extends Member> bClass = member1.getClass(); // (2)

Class<?> cClass = Class.forName("yoni.reflection.test"); // (3)

 

첫번째 방법으로는 클래스의 class 프로퍼티를 통해 회득하는 방법이다. 

두번째 방법으로는 인스턴스의 getClass() 메소드를 사용하는 방법이다.

세번째 방법으로는 Class 클래스 의 forName() 정적 메소드에 FQCN(Fully Qualified Class Name) 을 전달하여 해당 경로와 대응하는 클래스에 대한 Class 클래스 의 인스턴스를 얻는 방법이다.

 

이런 Class 의 객체는 Class 에 public 생성자가 존재하지 않아 우리가 직접 생성할 수 있는 방법은 없다. 대신 Class 의 객체는 JVM 이 자동으로 생성해준다. 

 

 

getXXX() vs getDelcaredXXX()

Class 객체의 메소드 중

getFields(), getMethods(), getAnnotations() 와 같은 형태와

getDeclaredFields(), getDeclaredMethods(), getDeclaredAnnotations() 와 같은 형태로 메소드가 정의되어 있는 것을 확인할 수 있다. 이 메소드들은 클래스에 정의된 필드, 메소드, 어노테이션 목록을 가져오기 위해 사용된다. 

 

이 둘은 무엇이 다른걸까? 

 

getXXX() 는 상속받은 클래스와 인터페이스를 포함하여 모든 public 요소를 가져온다. 예를 들면 getMethods() 는 해당 클래스가 상속받은, 그리고 구현한 인터페이스에 대한 모든 public 메소드를 가져온다.

 

반면, getDeclaredXXX() 는 상속받은 클래스와 인터페이스를 제외하고 해당 클래스에 직접 정의된 내용만 가져온다. 또한 접근 제어자와 상관없이 요소에 접근할 . 수있다. 예를 들어 getDeclaredMethods() 는 해당 클레스에만 직접 정의된 private, protected, public 메소드를 전부 가져온다. 

 

 

Constructor


Class 를 사용해서 생성자를 Constructor 타입으로 가져올 수 있다.

Constructor 는 java.lang.reflect 패키지에서 제공하는 클래스이며, 클래스 생성자에 대한 정보와 접근을 제공한다. 

리플렉션으로 생성자에 직접 접근하고, 객체를 생성해보자. 

// 생성자 가져오기
Class aClass = Class.forName("yoni.reflection.test")
Constructor constructor = aClass.getDeclaredConstructor();

 

위와 같이 Class 타입 객체의 getDeclaredConstructor() 를 사용하여 인자 없는 Constructor 를 획득할 수 있다. 

그렇다면, 인자가 있는 오버로딩 생성자를 가져오는 방법은 무엇일까? 간단하다. 타입에 일치하는 인자를 넣어주면 된다.

Class aClass = Class.forName("yoni.reflection.test")

Constructor noArgsConstructor = aClass.getDeclaredConstructor();
Constructor onlyNameConstructor = aClass.getDeclaredConstructor(String.class);
Constructor allArgsConstructor = aClass.getDeclaredConstructor(String.class, int.class);

 

 

public, private 등 모든 생성자를 가져오는 방법이 있고, 

Class aClass = Class.forName("yoni.reflection.test")

Constructor constructors[] = aClass.getDeclaredConstructor();
for (Constructor cons : constructors) {
    System.out.println("Get constructors in Child: " + cons);
}

// 출력
// Get constructors in Child: private yoni.reflection.test(java.lang.String)
// Get constructors in Child: public yoni.reflection.test()

 

public 생성자만 가져오는 방법도 있다.

Class aClass = Class.forName("yoni.reflection.test")

Constructor constructors[] = aClass.getConstructors();
for (Constructor cons : constructors) {
    System.out.println("Get public constructors in Child: " + cons);
}

// 출력
// Get public constructors in both Parent and Child: public yoni.reflection.test()

 

 

 

Method 


리플렉션을 사용하여 Method 타입의 오브젝트를 획득하여 객체 메소드에 직접 접근할 수 있다.

Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);

Method sayMyName = aClass.getDeclaredMethod("sayMyName");
sayMyName.invoke(member);
// 출력
// 내 이름은 요니

 

위와 같이 Method 타입의 invoke() 를 사용하여 메소드를 직접 호출할 수도 있다. 

 

 

 

Field 


리플렉션을 사용하여 Field 타입의 오브젝트를 획득하여 객체 필드에 직접 접근할 수 있다. 

Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);

for (Field field : aClass.getDeclaredFields()) {
    field.setAccessible(true);
    String fieldInfo = field.getType() + ", " + field.getName() + " = " + field.get(member);
    System.out.println(fieldInfo);
}

// 출력
// class java.lang.String, name = 요니
// int, age = 20

 

아래와 같이 set() 메소드를 사용하여 Setter 없이도 강제로 객체의 필드 값을 변경할 수도 있다. 

Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);

Field name = aClass.getDeclaredField("name");
name.setAccessible(true);
name.set(member, "Yoni"); // 필드값 변경

System.out.println("member = " + member);

// 출력
// member = Member{name='Yoni', age=20}

 

 

 

 

어노테이션 가져오기


Class<Member> aClass = Member.class;

Entitiy entityAnnotation = aClass.getAnnotation(Entitiy.class);
String value = entityAnnotation.value();
System.out.println("value = " + value);
// 멤버

 

위와 같이 getAnnotation() 메소드에 직접 어노테이션 타입을 넣어주면, 클래스에 붙어있는 어노테이션을 가져올 수 있다.

어노테이션이 가지고 있는 필드에도 접근할 수 있는 것을 확인할 수 있다.

 

 

리플렉션의 단점

일반적으로 메소드를 호출한다면, 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임에 클래스를 분석하므로 속도가 느리다.

JVM 을 최적화할 수 없기 때문이라고 한다. 그리고 이런 특징으로 인해 타입 체크가 컴파일 타임에 불가능하다. 또한 객체의 추상화가 깨진다는 단점도 존재한다. 

 

따라서 일반적인 웹 애플리케이션 개발자는 리플렉션을 사용할 일이 거의 없다. 보통 라이브러리나 프레임워크를 개발할 때 사용된다. 

따라서 정말 필요한 곳에서만 리플렉션을 사용하도록 하자.

 

 

 

 

References 

자바 리플렉션 (Reflection) 기초

리플렉션 (Reflection)이란 무엇일까?

728x90