컴파일 타임(Compile Time)에 특정 클래스의 정보를 알지 못하더라도 실행 시간(Run Time)에 동적으로 특정 클래스의 정보를 알아낼 수 있는 프로그래밍 기법이다.
즉, 구체적인 클래스 타입을 알지 못하더라도 해당 클래스의 이름을 알고 있다면 그 클래스의 객체를 생성할 수 있으며, 메서드의 이름을 알고 있다면 그 메서드를 호출할 수도 있다. 또한, Reflection을 사용하면 객체의 필드 값을 설정하거나 가져오는 등 객체의 내부 상태를 변경할 수 있습니다.
정리하자면 이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩(Dynamic Loading)하여 생성자, 멤버 필드, 멤버 메서드 등을 사용할 수 있도록 한다.
Java
는 컴파일 시점에 타입을 결정하는 정적인 언어이다.
- 정적 언어: 컴파일 시점에 타입을 결정 ex) Java, C, C++ 등
- 동적 언어: 런타임 시점에 타입을 결정 ex) JavaScript, Python, Ruby 등
정적 언어의 한계
- Java는 컴파일 시점에 타입을 결정하는 정적인 언어라서 런타임 시점에 타입을 결정할 수 없다.
- 이로 인해서 컴파일 단계에서 모든 타입이 결정되어 있어야 한다.
- 즉, 코드에 타입에 대한 모든 내용이 작성되어 있어야 한다.(런타임때 동적으로 결정될 수 없으므로)
- 이는 구체화에 의존한다는 의미이고, 이로 인해서 변경에 취약해진다.
이러한 정적 언어의 한계를 극복하기 위해서 런타임 시점에 동적으로 클래스 정보에 접근하기 위해서 리플렉션을 사용한다.
class Child {
public String name = "I'm child";
public Child() {
}
private Child(String name){
this.name = name;
}
public String callName(){
return name;
}
}
// 클래스 이름으로부터 클래스 조회
Class<?> clazz = Class.forName("Child");
System.out.println("clazz.getName() = " + clazz.getName()); //clazz.getName() = Child
// 해당 클래스의 생성자 조회
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
System.out.println("declaredConstructor.getName() = " + declaredConstructor.getName()); //declaredConstructor.getName() = Child
// 해당 클래스의 메서드 조회
Method method = clazz.getMethod("callName");
System.out.println("method = " + method); //method = public java.lang.String Child.callName()
//해당 클래스의 필드 조회
Field field = clazz.getField("name");
System.out.println("field = " + field); //field = public java.lang.String Child.name
코드를 작성할 시점에 어떤 타입의 클래스를 사용할 지 모르는 경우에 리플렉션을 사용한다.
아래 예시처럼 개발자가 직접 사용하는 경우는 드물다.
class Music {
private String singer;
public String getSinger(){
return this.singer;
}
}
public class Main {
public static void main(String[] args) {
Object music = new Music();
music.getSinger(); //사용 불가
}
}
위의 예시는 music 인스턴스가 Object 타입이기 때문에 Music 클래스에 선언된 getter 함수들을 사용할 수 없다.
이렇게 (Object)music이 구체적인 클래스 타입을 알지 못할 때 Music 메서드를 사용 가능하도록 해주는 기능이 Reflection이다.
Object music = new Music("John");
//Reflection 사용 시작
Class c = Music.class;
Method getSinger = c.getMethod("getSinger");
String singer = (String) getSinger.invoke(music, null);
System.out.println("singer = " + singer); //singer = John
위 예시처럼 Java Reflection을 개발자가 코딩을 하면서 직접 사용하는 일은 극히 드물다. 자신이 작성한 코드의 구체적인 클래스를 모를일이 거의 없기 때문이다.
리플렉션은 주로 스프링 프레임워크, ORM 기술인 하이버네이트, jackson 라이브러리 등에 사용된다고 한다.
스프링 프레임워크의 경우 프레임워크 입장에서는 개발자가 어떤 클래스를 만들었는지 모른다.(클래스 내부의 정보를 모름)
이럴때 동적으로 해결해주기 위해서 리플렉션을 사용한다.
Spring Framework
- Spring Container의 BeanFactory에서 활용한다.
- Bean은 애플리케이션이 실행한 후 런타임에 객체가 호출될 때 동적으로 객체의 인스턴스를 생성하는데 이 때 Spring Container의 BeanFactory에서 리플렉션을 사용한다.
Spring Data JPA
- 동적으로 객체 생성 시 Reflection API를 활용한다.
- Entity에 기본 생성자가 필요한 이유는?
- Reflection API로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이다. 그래서 기본 생성자가 반드시 있어야 객체를 생성할 수 있는 것이다. 기본 생성자로 객체를 생성만 하면 필드 값 등은 Reflection API로 넣어줄 수 있다.
- 만야 기본 생성자가 없다면 java Reflection은 해당 객체를 생성할 수 없기 때문에 JPA의 Entity에는 기본 생성자가 꼭 필요하다.
지나친 사용은 성능 이슈를 야기할 수 있다. 반드시 필요한 경우에만 사용할 것 -> 이미 인스턴스를 만들었음에도 불구하고 굳이 필드와 리플렉션을 이용해서 접근하거나 사용할 경우
컴파일 타임에 확인되지 않고 런타임 시에마 발생하는 문제를 만들 가능성이 있다.(컴파일 시 타입 체킹할 수 없으므로 런타임시 잘못된 파라미터로 인해 런타임 에러가 발생하기 쉽다.)
외부에서 접근할 수 없는 private 멤버 변수에도 접근할 수 있다.