런타임시에 클래스, 메서드, 필드 등의 정보를 동적으로 확인하거나 수정할 수 있게 해주는 기능
프로그램에서 임의의 클래스에 접근할 수 있다.
즉 Class객체가 주어지면 그 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field인스턴스를 가져올 수 있다.
또한 해당 가져온 인스턴스들을 통해서 실제 생성자, 메서드, 필드를 조작할 수도 있다. 이 인스턴스를 이용해서 해당 클래스의 인스턴스를 생성하거나, 메서드를 호출하거나, 필드에 접근할 수 있다는 의미이다.
코드 작성 및 컴파일 시점에 어떤 클래스가 존재하는지 몰라도, 실행중에 클래스 이름을 문자열로 지정해 해당 클래스를 찾고 사용할 수 있다.
//컴파일 시점에 class가 이미 존재하고 있다.
MyClass obj = new MyClass();
obj.sayHello();
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 컴파일 시점에 MyClass를 몰라도 런타임에 클래스 이름만 알고 있으면 로드 가능
String className = "MyClass"; // 클래스 이름을 문자열로 지정
// Class.forName()을 사용하여 클래스 동적 로드
//클래스 이름만 알고 있다면? 실행 중에 클래스를 찾고 객체를 생성하거나 메서드 호출 가능
Class<?> clazz = Class.forName(className);
// 생성자 호출로 객체 생성
Object obj = clazz.getDeclaredConstructor().newInstance();
// 메서드 호출
clazz.getMethod("sayHello").invoke(obj);
}
}
컴파일타임 타입 검사의 이점을 누릴 수 없다.
프로그램이 리플렉션 기능을 이용해 존재하지 않거나 접근할 수 없는 클래스에 접근하다면 런타임 에러가 발생한다.
코드가 지저분해진다.
성능이 떨어진다.
일반 메서드 호출보다 훨씬 느리다.
리플렉션의 장점과 단점을 보고 파악하면 리플렉션은 컴파일 타임에 알 수 없는 클래스를 사용할 때 적합하다.
그렇다면 그런 상황은 어떤게 있을까?
String pluginClassName = "com.example.MyPlugin"; // 외부 플러그인의 클래스 이름
Class<?> pluginClass = Class.forName(pluginClassName);
Object pluginInstance = pluginClass.getDeclaredConstructor().newInstance();
intellij, vsCode의 IDE 플러그인 시스템, 게임 엔진의 Mod 시스템, 애플리케이션의 확장 모듈 같은 것이 있다.
@Component , @Service, @Repository
등의 어노테이션을 스캔하고, 런타임에 객체를 생성해서 의존성 주입을 수행한다.
컴파일 타임에는 어떤 클래스가 등록될 지 몰라도 런타임에 찾아낸다.
Class<?> clazz = Class.forName("com.example.MyService");
Object service = clazz.getDeclaredConstructor().newInstance();
public class ConstFile {
private static final String IMCONST = "im const";
private static String hello() {
return "hello I'm in the private method!";
}
}
여기서 ConstFile은 외부에서 접근할 수 없도록 private 접근제한자를 통해 생성했다.
private, static (객체 생성과 상관없이 클래스에 직접 속함 ), final 값 변경 불가
즉 외부에서 필드에 접근하거나 값을 읽을 수 없고 마찬가지로 메서드도 외부에서 읽을 수 없다.
public class ReflectionTest {
public static void main(String[] args) {
Class<?> constClass = null;
try {
constClass = Class.forName("com.example.demoTest.reflection.ConstFile");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
try {
Constructor<?> constructor = constClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object instance = constructor.newInstance();
System.out.println("instance created : " + instance);
//private field 가져오기
Field staticField = constClass.getDeclaredField("IMCONST");
staticField.setAccessible(true);
String staticValue = (String)staticField.get(null);
System.out.println("staticValue : " + staticValue);
//private 메서드 가져오기
Method privateMethod = constClass.getDeclaredMethod("hello");
privateMethod.setAccessible(true);
//메서드 실행해보기
String result = (String)privateMethod.invoke(instance);
System.out.println(result);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
staticValue : im const
hello I'm in the private method!
process time = 18 ms
ConstPublic contents = ConstPublic
ConstPublic Method = method public
process time = 10 ms
보면 알다싶이 리플렉션을 사용하지 않은 코드는 10ms초 리플렉션을 사용한 코드는 18ms초인것을 확인할 수 있다.
이는 리플렉션은 런타임시에 클래스를 찾아내고 수행하기 때문이다. 그래서 시간이 조금 더 걸리는 것 성능저하의 원인이 될 수 있다.
여기서 리플렉션을 사용하지 않은 코드는 따로 ConstPublic이라는 class를 만들고 private이 아닌 public으로 설정해서 인스턴스를 생성할 수 있도록 설정했다.
리플렉션은 런타임에 존재하지 않을 수도 있는 다른 클래스, 메서드를 사용하게 된다. 그래서 존재하지 않는 필드, 메서드를 사용하기 때문에 try~catch()을 통해서 예외를 처리해야한다.
리플렉션은 되도록 객체 생성시에만 사용하고, 생성한 객체를 이용할 때에는 적절한 인터페이스나 컴파일 타임에 알 수 있는 상위 클래스로 형변환해서 사용해야 한다.
사유는 타입 안정성, 성능 때문이다.
예시를 먼저 살펴보자
public interface MyService {
void execute();
}
public class MyServiceImpl implements MyService {
@Override
public void execute() {
System.out.println("executed!!!");
}
}
public class MyServiceReflection {
public static void main(String[] args) {
String className = "com.example.demoTest.reflection.MyServiceImpl";
try {
Class<?> clazz = Class.forName(className);
//객체 생성
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object obj = constructor.newInstance();
//인터페이스로 형 변환하기
if (obj instanceof MyService) {
MyService myService = (MyService)obj;
myService.execute();
} else {
throw new IllegalArgumentException("provied class doesn't implement MyService");
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
타입 안전성
변수, 메서드, 객체 등이 컴파일 타임에 올바른 타입을 사용하도록 보장한다. 컴파일 타임에 타입을 알 수 없어서 런타임에 오류가 발생할 수 있다.
리플렉션 최소화
성능 오버헤드와 가독성 저하를 일으킬 수 있다. 즉 객체 생성 단계에서만 사용하고 이후에는 정적 타입으로 처리한다.