컴파일 시간이 아닌 실행 시간에 클래스에 대한 정보를 동적으로 가져와서 사용하는 기술
자바에서 컴파일러가 소스 코드를 바이트코드로 바꾸면 클래스 로더(class loader)가 바이트코드를 해석해서 JVM 내 메모리 영역에 저장한다. 리플렉션은 JVM 메모리 영역에 저장된 클래스의 정보를 꺼내서 필요한 정보들(필드, 메서드, 생성자)을 사용하는 기술이다.
컴파일 에러가 아닌 런타임 에러가 발생하기 때문에 상당한 주의가 필요하며 되도록 사용을 하지 않는것이 좋습니다.
대표적으로는 Spring 프레임워크의 어노테이션 같은 기능들이 리플렉션을 이용하여 프로그램 실행 도중 동적으로 클래스의 정보를 가져와서 사용한다.
Spring은 리플렉션을 이용하여 런타임 시에 필요한 Bean을 동적으로 생성한다.
프레임워크나 라이브러리에서 리플렉션이 많이 사용되는 이유는 개발자가 직접 작성한 코드에서는 객체의 타입을 쉽게 파악할 수 있지만, 프레임워크나 라이브러리에서는 컴파일 시점에 객체의 타입을 확인하기 어렵다. 따라서 런타임 실행 중에 동적으로 해결하기 위해서 리플렉션을 사용한다.
자바에서는 java.lang.Class가 대표적인 예시
C나 C++, 파스칼은 리플렉션을 지원하지 않는다.
Class는 실행 중인 자바 어플리케이션의 클래스와 인터페이스의 정보를 가진 클래스다.
Class 객체는 JVM에 의해 자동으로 생성된다. 따라서 public 생성자가 없다.
// Object 클래스의 getClass() 메서드를 호출하면 Class 클래스형의 객체를 가져올 수 있다.
String s = new String("안녕하세요");
Class c = s.getClass();
// 여기서 객체 c는 Class 클래스형의 객체다. 즉, c는 String.class를 뜻한다.
Class c = s.getClass();
Class c = String.class;
// s의 Class 클래스는 String.class다.
Class<?> catClass = Cat.class;
Cat myCat = new Cat("나비");
Class myCatClass = myCat.getClass();
Class<?> catClass = Class.forName("org.example.cat");
Class 클래스는 각각의 클래스에 대한 정보를 가져오는 메서드를 제공한다.
이 메서드가 제공하는 정보는 두 가지다.
자바에서는 모든 .class 파일 하나마다 java.lang.Class 객체 하나씩이 생성된다. 이 Class 객체는 모든 .class 파일의 정보를 가지고 있다.
모든 .class 파일은 이 클래스를 처음 사용하는 시점에 동적으로 클래스 적재기(class loader)를 통해 JVM에 적재된다.
처음 사용하는 시점이라면 해당 .class 파일에서 static을 처음으로 사용하는 때를 말하는데, 생성자도 클래스(static) 메서드다.
그래서 new 연산자를 사용하게 되면 적재된다고 보면 된다. 이를 동적 로딩이라 한다.
.class 파일의 정보와 Class 객체는 JVM에 런타임 데이터 영역(runtime data area)의 메서드 영역에 저장된다.
이들 정보를 java.lang.reflect 패키지에 정의된 클래스들을 통해 접근하게 해준다.
Class 클래스를 사용하여 객체의 정보를 분석하는 기술이 리플렉션이다.
거의 대부분의 리플렉션은 프로그램 내부적으로 사용된다.
Class 클래스는 실행 중에 클래스의 정보를 분석한다.
java.lang.reflect 패키지에서는 속성, 메서드, 생성자 같은 class들이 있고, 생성자를 사용해 새로운 객체를 생성하고, getter 메서드와 setter 메서드를 사용해 속성값을 읽거나 수정할 수 있다.
매개 변수(parameter), 반환형, 접근 제어자 등 class에 관련된 모든 정보를 가져 올 수 있다.
심지어 private로 캡슐화(encapsulation)된 불변(immutable)의 것까지도 setAccessible(true)를 통해 접근 가능하다.
리플렉션을 통해 어노테이션을 붙일 수 있는 클래스나 메서드, 파라미터 정보를 가져온다.
리플렉션의 getAnnotations, getDeclaredAnnotations 등의 메서드를 통해 어노테이션의 유무를 확인한다.
어노테이션이 붙어 있다면 해당 로직을 수행한다.
Spring에서는 @Autowired만 붙이면 쉽게 객체에 해당하는 의존성을 주입(Dependency Injection)을 해줄 수 있습니다.
내부 노출 : 리플렉션을 사용하면 접근 지시자를 무시할 수 있기 때문에 추상화가 깨지고 플랫폼 업그레이드 시 동작이 변경될 수 있다.
성능 저하 : 리플렉션을 통한 작업은 일반 작업보다 훨씬 느리다.
컴파일 불가 : 컴파일시 타입 검사나 예외 검사를 할 수 없어 런타임에 문제가 발생할 수 있다.
public class Cat {
private final String name;
private int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private void meow() {
System.out.println("Meow!");
}
private static void meow2() {
System.out.println("Meow2!");
}
}
import java.lang.reflect.Field;
public static void main(String[] args) throws Exception{
Cat myCat = new Cat("나비", 6);
Field[] myCatFields = myCat.getClass().getDeclaredFields();
// Cat 클래스의 속성 찾기
for(Field field : myCatFields)
System.out.println(field.getName());
// name
// age
// Cat의 이름이 private final이지만 변경하기
for(Field field : myCatFields)
if(field.getName().equals("name")){ // 이게 reflection을 사용하지 말아야 하는 이유이기도 한데, 다른 개발자가 아무런 경고 없이 Cat 클래스 속성 name을 nickname으로 바꾸면 코드가 동작하지 않는다.
field.setAccessible(true); // 이게 없으면 바뀌지 않는다.
field.set(myCat, "네로"); // checked exception
}
// Cat의 함수 호출하기
Method[] myCatMethods = myCat.getClass().getDeclaredMethods();
for(Method method : catMethods){
if(method.getName().equals("meow")){
method.setAccesible(true); // 만약 meow 메서드가 public이였으면 필요없음
method.invoke(myCat);
}
}
for(Method method : catMethods){
if(method.getName().equals("meow2")){
method.setAccesible(true);
method.invoke(null); // static은 객체를 생성한 후 호출할 필요없이 바로 호출하면 되서 null로도 작동한다.
}
}
}
public 메서드들만 불러온다.
상위 클래스와 상위 인터페이스에서 상속한 메서드들을 포함해 모든 메서드들을 불러온다.
접근 제어자 상관 없이 모든 메서드들을 불러온다.
직접 클래스에서 선언한 메서드만을 불러온다.