📌 리플렉션(Reflection)
구체적인 클래스 타입을 알지 못하더라도 해당 클래스의 생성자, 메서드, 필드에 해당하는 Constructor, Method, Field 인스턴스를 가져올 수 있고,
해당 인스터스들로 그 클래스의 member name, field type, Method signature 등을 가져올 수 있다.
- 동적으로 클래스를 사용해야할 때 사용한다.
- Compile time에는 어떠한 클래스를 사용해야 할 지 모르겠으므로, Runtime에 가져와 실행하는 경우
- 각각에 연결된 실제 생성자, 메서드, 필드를 조작할 수 있다.
- Reflection으로 가져올 수 있는 정보 : Class, Constructor, Method, Field
✒️ 구체적인 클래스 타입을 알지 못한다?
Runner의 타입을 몰라서 Object 타입을 사용하면 run() 메서드를 사용할 수 없다.
public static void main(String[] args) {
Object runner = new Runner();
runner.run(); // 컴파일 오류 !
}
설령 내가 작성하더라도 클래스 타입을 알 수 없는 경우가 있다.
• 코드를 작성할 때는 어떤 타입 클래스가 사용될 지 예측할 수 없는 경우
• 런타임에서 실행되고 있는 클래스 정보를 이용해야 하는 경우
∘ IDE 자동 완성 : 사용자가 어떤 클래스를 작성했는지 IDE 개발 시점에는 모른다.
∘ Java Class Loader : 동적으로 로딩할 클래스 파일의 클래스들이 어떤 것들이 있을 지 모른다.
∘ @Autowired : 어노테이션을 만드는 시점에는 어떤 클래스에 해당 어노테이션이 붙을 지 모른다.
이게 가능한 이유는 Java 클래스 파일 정보는 Compile되면 클래스 파일로 변환되어 Memory의 Static 영역에 위치하게 되기 때문이다.
static 영역에 있는 정보는 언제든지 접근할 수 있기 때문에, class 이름만 알면 정보들을 가져올 수 있다.
🟡 Reflection 사용 방법
1️⃣ Class 찾기
Class clazz1 = (클래스명).class; // ex. String.class
Class clazz2 = Class.forName("(패키지 네임이 포함된 클래스 명)"); // ex. java.util.HashMap
Class clazz3 = (인스턴스명).getClass();
- Class 객체는 클래스 혹은 인터페이스를 가리킨다.
- Class 객체는 여러 메서드를 제공한다.
- getXXX() : 상속받은 클래스와 인터페이스를 포함하여 모든 public 요소를 가져온다.
- getFields(), getMethods(), getAnnocations(), getName(), ...
- getDeclaredXXX() : 상속받은 클래스와 인터페이스를 제외하고 해당 클래스에 직접 정의된 내용만 가져온다.
- getDeclaredFields(), getDelcaredMethods(), getDeclaredAnnocations(), ...
- getXXX() : 상속받은 클래스와 인터페이스를 포함하여 모든 public 요소를 가져온다.
- Class 객체는 public 생성자가 존재하지 않아서 JVM이 자동으로 생성해준다.
2️⃣ Constructor 찾기
Constructor<?> cons = (클래스 객체).getDeclaredConstructor();
// 인스턴스 생성
Object obj = cons.newInstance();
// 타입 캐스팅
(Something) something = (Something) cons.newInstance();
- Class 타입 객체의 getDeclaredConstructor()로 생성자를 얻을 수 있다.
Constructor<?> noArgsConstructor = cl.getDeclaredConstructor();
Constructor<?> onlyNameConstructor = cl.getDeclaredConstructor(String.class);
Constructor<?> allArgsConstructor = cl.getDeclaredConstructor(String.class, int.class);
// 파라미터가 존재하는 생성자로 인스턴스 생성
Foo foo = (Foo) allArgsConstructor.newInstance("Foo", 42);
// private 생성자로 객체 생성
Bar bar = (Bar) noArgsConstructor.setAccessible(true);
- 생성자에 매개변수가 있다면 대응하는 타입을 파라미터로 전달하면 된다.
- private 생성자의 경우 setAccessible(true)를 사용해 접근할 수 있다.
3️⃣ Method 찾기
// 매개변수가 없는 경우 null을 넣어준다.
Method method1 = cl.getDeclaredMethod("method1", null);
// 인자가 하나인 경우, 매칭되는 타입
Method method2 = cl.getDeclaredMethod("method4", int.class);
// 인자가 두 개 이상인 경우
Class[] partypes = new Class[2];
partypes[0] = int.class; partypes[1] = String.class;
Method method3 = cl.getDeclaredMethod("method4", partypes);
// public 메서드만
Method[] methods4 = cl.getMethods();
- 매개변수에 대응되는 필드 타입을 명시해주면 Method 객체를 얻어 직접 접근이 가능하다.
✒️ method 실행 : Method.invoke()
/* 매개변수가 없는 경우 */
Method method1 = cl.getDeclaredMethod("method1", null);
method1.setAccessible(true); // private 메서드에 접근하기 위해 접근성을 변경
// 인스턴스 생성
Object instance = cl.newInstance();
// method1 메서드 호출
Object result1 = method1.invoke(instance, (Object[]) null);
/* 매개 변수가 2개 이상인 경우 */
Class[] partypes = new Class[2];
partypes[0] = int.class; partypes[1] = String.class;
Method method3 = cl.getDeclaredMethod("method4", partypes);
method3.setAccessible(true); // private 메서드에 접근하기 위해 접근성을 변경
// 인스턴스 생성
Object instance = clazz.newInstance();
// method4 메서드 호출
Object[] args = {42, "Hello"}; // int 타입과 String 타입의 두 개의 인자
Object result3 = method3.invoke(instance, args);
- method를 실행 시킬 객체 정보를 위해 객체 생성(new 키워드) 단계가 필요하다. → newInstance() 활용
- 메서드 인자를 채워 invoke()로 Method 객체 실행
4️⃣ Field 찾기
Class<Foo> cl = Foo.class;
Foo foo = new Foo("Foo", 42);
// 필드 조회
for (Field field : cl.getDeclaredFields()) {
field.setAccessible(true);
String fieldInfo = field.getType() + ", " + field.getName() + " = " + field.get(member);
System.out.println(fieldInfo);
}
// 필드 수정
Field name = cl.getDeclaredField("name");
name.setAccessible(true);
name.set(foo, "Bar");
- Field 타입의 Obejct를 얻어 필드에 직접 접근할 수 있으며, Setter가 없이도 강제로 바꿀 수 있다.
📌 리플렉션 단점
- 컴파일 타임 검사가 주는 이점을 누릴 수 없다.
- 예외 검사도 마찬가지다.
- 프로그램이 Reflection 기능으로 존재하지 않는, 혹은 접근할 수 없는 method 호출 시 Runtime error가 발생한다.
- 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
코드 분석 도구(IntelliJ 자동 완성 기능), 의존 관계 주입 프레임워크(Spring annotation)처럼 Reflection을 써야 하는 복잡한 Application조차 사용 빈도수를 줄이고 있다. 단점이 너무 명백하기 때문이다.
📌 리플렉션의 단점을 피하는 방법
💡 아주 제한된 형태로만 사용하라.
아래는 객체 생성 부분에만 국한된 리플렉션 사용법이다.
public class Main {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// 클래스 이름을 Class 객체로 변환
Class<? extends Set<String>> cl = null; // Set<String> 확장 객체 받을 예정
try {
cl = (Class<? extends Set<String>>) // 비검사 형변환
Class.forName(args[0]); // 클래스 이름을 Class 객체로 변환
} catch (ClassNotFoundException e) {
fatalError("클래스를 찾을 수 없습니다.");
}
// 생성자를 얻는다.
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor(); // 생성자 호출
} catch (NoSuchMethodException e) {
fatalError("매개변수 없는 생성자를 찾을 수 없습니다.");
}
// 집합의 인스턴스를 만든다.
Set<String> s = null;
try {
s = cons.newInstance(); // Set<String> 인스턴스 생성
} catch (IllegalAccessException e) {
fatalError("생성자에 접근할 수 없습니다.");
} catch (InstantiationException e) {
fatalError("클래스를 인스턴스화할 수 없습니다.");
} catch (java.lang.reflect.InvocationTargetException e) {
fatalError("생성자가 예외를 던졌습니다: " + e.getCause());
} catch (ClassCastException e) {
fatalError("Set을 구현하지 않은 클래스입니다.");
}
// 생성한 집합을 사용한다.
s.addAll(java.util.Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
}
대부분 Reflection 기능은 이 정도만 사용해도 충분하다.
- 컴파일 타임에 이용할 수 없는 클래스라도 적절한 인터페이스나 상위 클래스는 이용할 수 있을 것이다. (Item 64)
- 이런 경우는 Reflection을 인스턴스 생성에만 쓰고, 해당 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하라.
- 위의 예시에서는 TreeSet, HashSet 무엇이 사용될 지는 모르겠으나, 인터페이스 Set<String>을 참조하도록 했다.
- 구현체 종류에 상관 없이 중복은 모두 제거될 것이다.
- TreeSet이면 알파벳 순서, HashSet이면 무작위 순서가 될 것이다.
- 객체가 일단 만들어지면 그 후의 코드는 기존 Set 인스턴스 사용 방법과 동일하다.
- 비검사 형변환 경고가 뜨지만 (Set을 구현하지 않았더라도) 무조건 성공한다.
- Runtime에서는 해당 클래스가 Set 인터페이스를 구현했는지 확인하지 않는다.
- 대신 인스턴스를 생성하려 할 때 ClassCastException을 던지게 된다.
🟡 (제대로 된) 리플렉션의 장점
- 손쉽게 제네릭 집합 테스터 프로그램을 만들 수 있다.
- 명시한 Set 구현체를 공격적으로 조작하며, Set 규약을 잘 지키는지 검사할 수 있다.
- 제네릭 집합 성능 분석 도구로 사용할 수도 있다.
🟡 위에서 알 수 있는 리플렉션의 단점
1️⃣ 런타임에 총 여섯 가지나 되는 예외를 던질 수 있다.
try {
s = cl.newInstance(); // Set<String> 인스턴스 생성
} catch (ReflectiveOperationException e) {
fatalError("클래스를 인스턴스화할 수 없습니다.");
}
- 그나마 Java 7부터는 RelectiveOperationException으로 줄일 수 있다.
- 인스턴스를 Reflection 없이 생성했다면 모두 컴파일 타임에 잡아낼 수 있었을 예외들이다.
2️⃣ 클래스 이름만으로 인스턴스를 생성해내기 위해 코드가 너무 길어진다.
- Reflection이 아니었다면 생성자 호출 한 줄이었으면 끝났을 일이다.
📌 Relection이 적합한 경우
- 매우 드물긴 하나, Runtime에 존재하지 않을 수도 있는 다른 Class, method, filed와의 Dependency 관리에 적합하다.