Java Reflection이란?
Java Reflection은 언어이자 JVM의 기능이며, 앱이 실행하는 동안에 클래스와 객체 정보를 추출할 수 있다.
Reflection API로 유연한 코드를 작성할 수 있으며, 프로그램 실행 단계에서 다양한 소프트웨어 컴포넌트를 연결하고 소스 코드를 수정하지 않고 새로운 프로그램 순서를 만들어 낼 수 있다. 또한 Reflection으로 다목적 알고리즘도 작성할 수 있다.
우리가 보통 작성하고 프로그램은 데이터를 넣고 분석해서 작업을 수행하고 출력값을 돌려준다.
반면에 Reflection으로 프로그램을 작성하면 데이터와 코드를 모두 입력값으로 인식하고 분석해 작업을 수행하고 출력값을 낸다.
Reflection은 프로그램을 실행하면서 앱 객체와 클래스를 분석하고 앱 객체와 클래스를 입력값으로 사용할 수 있어 뛰어난 라이브러리와 프레임워크를 만들고 훌륭한 소프트웨어를 설계할 수 있다.
Java Reflection으로 만들어진 것들
라이브러리
- JUnit
- GSON, Jackson
JSON문자열을 Jackson이나 GSON같은 라이브러리는 Reflection을 사용해서 클래스를 확인하고 필드를 전부 분석하고 필드명에 따라 JSON 문자열에서 가져온 값만 입력한다.
프레임워크
- Spring
의존성 주입 같은 경우 Reflection을 통해 일어나게 된다.
Java Reflection 단점
- 보이지 않는 구조에 접근하기에 난이도가 높다
- 목적에 맞지 않게 사용되면 코드를 변경해야 한다.
- 코드가 늦게 실행될 수도 있다.
- Reflection 코드가 앱을 망가뜨려 복구를 할 수 없게 될 수도 있다.
Class<T> - Java Reflection API의 진입점
- 객체의 런타임에 관한 정보
- 앱에 있는 특정한 클래스 정보
- 메서드와 필드 정보
- 어떤 클래스를 확장하는지
- 어떤 인터페이스를 실행하는지
등 Class<T>는 다양한 정보들을 담고있다.
이러한 Class객체를 얻기 위해 세 가지 방법을 사용할 수 있다.
1. Object.getClass()
String stringObj = "string";
Car car = new Car();
Map<String, Integer> map = new HashMap<>();
Class<?> stringClass = stringObj.getClass();
Class<Car> carClass = car.getClass();
Class<?> mapClass = map.getClass();
Object의 getClass 메서드를 통해서 런타임 타입을 알 수 있다.
따라서 마지막 map.getClass()는 Map 인터페이스가 아닌 런타임 타입인 HashMap클래스를 나타내게된다.
2. 타입 이름 마지막에 ".class" 추가
Class<String> stringClass = String.class;
Class<Car> carClass = Car.class;
Class<?> mapClass = Map.class;
해당 메서드는 클래스의 인스턴스가 없을 때 사용된다.
Class booleanType = boolean.class;
Class intType = int.class;
Class doubleType = double.class;
이 메서드를 사용해서 원시 타입 정보를 얻을 수 있다.
메서드의 매개변수나 클래스 멤버는 원시 타입일 수 있기 때문에 클래스가 아니라 Java에 있는 모든 타입 정보를 얻어야 하기 때문에 사용된다.
3. Class.forName(...)
정적 메서드인 Class.forName을 사용한다. 해당 메서드를 사용해면 패키지 명을 포함한 클래스 경로에서 동적으로 클래스를 찾을 수 있다.
Class<?> stringType = Class.forName("java.lang.String");
Class<?> carType = Class.forName("vehicles.Car");
Class<?> engineType = Class.forName("vehicles.Car$Engine"); // 내부 클래스 정보에 접근할 땐 $ 사용
이 메서드는 런타임 단계에서 해당 클래스를 찾기 때문에 이름을 잘못 입력했다던지 찾으려는 클래스가 없다던지 할 수도 있기에 ClassNotFoundException 이라는 런타임 오류를 던진다.
그렇기에 위 세 방법 중 Class.forName이 가장 위험하다.
Class<T> 타입을 사용한 Class 정보 출력
private static void printClassInfo(Class<?> ...classes) {
for(Class<?> clazz : classes) {
System.out.printf("class name: %s, class package name: %s%n", clazz.getSimpleName(), clazz.getPackageName());
Class<?>[] implementedInterfaces = clazz.getInterfaces();
for(Class<?> implementedInterface : implementedInterfaces) {
System.out.printf("class %s implements: %s%n", clazz.getSimpleName(), implementedInterface.getSimpleName());
}
System.out.println("Is Array: " + clazz.isArray());
System.out.println("Is Primitive: " + clazz.isPrimitive());
System.out.println("Is Enum: " + clazz.isEnum());
System.out.println("Is interface: " + clazz.isInterface());
System.out.println("Is Anonymous: " + clazz.isAnonymousClass());
}
}
여러 정보들을 출력할 수 있는 printClassInfo 함수를 만들고 해당 클래스에 여러 타입의 클래스 정보를 넘겨보자
Class<String> stringClass = String.class;
/*
class name: String, class package name: java.lang
class String implements: Serializable
class String implements: Comparable
class String implements: CharSequence
class String implements: Constable
class String implements: ConstantDesc
Is Array: false
Is Primitive: false
Is Enum: false
Is interface: false
Is Anonymous: false
*/
// ==============================================================================
Map<String, Integer> mapObject = new HashMap<>();
Class<?> hashMapClass = mapObject.getClass();
/*
class name: HashMap, class package name: java.util
class HashMap implements: Map
class HashMap implements: Cloneable
class HashMap implements: Serializable
Is Array: false
Is Primitive: false
Is Enum: false
Is interface: false
Is Anonymous: false
*/
// ==============================================================================
private static class Square implements Drawable {
@Override
public int getNumberOfCorners() {
return 4;
}
}
Class<?> squareClass = Class.forName("reflectionTest.FunctionTest$Square");
/*
class name: Square, class package name: reflectionTest
class Square implements: Drawable
Is Array: false
Is Primitive: false
Is Enum: false
Is interface: false
Is Anonymous: false
*/
// ==============================================================================
private interface Drawable {
int getNumberOfCorners();
}
var circleObject = new Drawable() {
@Override
public int getNumberOfCorners() {
return 0;
}
};
/*
class name: , class package name: reflectionTest
class implements: Drawable
Is Array: false
Is Primitive: false
Is Enum: false
Is interface: false
Is Anonymous: true
*/
// ==============================================================================
Collection.class;
/*
class name: Collection, class package name: java.util
class Collection implements: Iterable
Is Array: false
Is Primitive: false
Is Enum: false
Is interface: true
Is Anonymous: false
*/
// ==============================================================================
boolean.class;
/*
class name: boolean, class package name: java.lang
Is Array: false
Is Primitive: true
Is Enum: false
Is interface: false
Is Anonymous: false
*/
// ==============================================================================
int[][].class;
/*
class name: int[][], class package name: java.lang
class int[][] implements: Cloneable
class int[][] implements: Serializable
Is Array: true
Is Primitive: false
Is Enum: false
Is interface: false
Is Anonymous: false
*/
// ==============================================================================
private enum Color {
BLUE,
RED,
GREEN
}
Color.class;
/*
class name: Color, class package name: reflectionTest
Is Array: false
Is Primitive: false
Is Enum: true
Is interface: false
Is Anonymous: false
*/
Constructor<?> - 생성자 설정
Java Class의 생성자는 Constructor<?> 클래스의 인스턴스로 나타낸다.
클래스는 생성자를 여러개를 가질 수 있기에 파라미터의 개수, 타입에 상관없이 Constructor 객체는 class의 모든 생성자를 포함한다.
생성자 가져오기
- Class.getDeclaredConstructor():
생성자를 얻을 수 있는 가장 유용한 메서드로 접근 제어자에 상관없이 모든 생성자들을 반환한다. - Class.getConstructors():
public으로 선어된 생성자만 반환한다.
Constructor<Person> constructor = Person.class.getConstructor(String.class, int.class);
위의 두 메서드를 사용해서 생성자를 가져올 때 매개벼수의 타입을 안다면 타입 목록을 전달해서 생성자를 특정해 가져올 수 있다.
이제 위 메서드를 사용해서 생성자들과 파라미터들을 간단하게 출력해보자.
// 클래스
public class Person {
public Person(Address address, String name, int age) {
// ...
}
public Person(String name, int age) {
// ...
}
public Person(String name) {
// ...
}
public Person() {
// ...
}
public class Address {
// ...
}
}
// 생성자의 parameter type 출력
Constructor<?>[] constructors = Person.class.getDeclaredConstructors();
System.out.printf("class %s has %d declared constructors\n", Person.class.getSimpleName(), constructors.length);
for (Constructor<?> constructor : constructors) {
Class<?>[] parameterType = constructor.getParameterTypes();
List<String> parameterTypeNames = Arrays.stream(parameterType)
.map(Class::getSimpleName)
.toList();
System.out.println(parameterTypeNames);
}
// ===========
// 결과 :
/*
class Person has 4 declared constructors
[]
[String]
[String, int]
[Address, String, int]
*/
Constructor.newInstance(Object ...args)
생성자에 선언된 매개변수의 순서대로 가변인수를 받아 객체의 인스턴스를 생성하는 메서드이다.
인수가 올바른 타입과 올바른 순서로 전달되어 생성자에 접근할 수 있다면 특정 클래스 생성자를 호출한다.
Constructor<String> constructor = String.class.getConstructor(String.class);
String strangeStr = constructor.newInstance("Hello, World!");
위 처럼 이상하게 String을 생성할 수도 있다.
이제 위의 개념들을 이욯해서 어떤 클래스의 객체든 메서드에 전달한 인수에 따라 알맞은 생성자를 찾아 생성하는 단일 팩토리 메서드를 만들어보자.
public static <T> T createInstanceWithArguments(Class<T> clazz, Object... args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
for (Constructor<?> constructor : clazz.getConstructors()) {
// 파라미터의 개수가 같으면
if (constructor.getParameterTypes().length == args.length) {
return (T) constructor.newInstance(args);
}
}
// 해당하는 생서자 찾지 못함
return null;
}
객체를 만들려고하는 객체의 Class 정보를 받고, 매개 변수들을 가변인자로 받는다. 그런 후 받은 객체의 생성자들을 모두 순회하면서 받은 매개 변수의 개수와 생성자의 파라미터의 개수가 같은 생성자를 찾아서 받은 매개 변수를 넘겨준다.
(단, 여기서는 오버로딩을 고려하지 않았다. 따라서 오버로딩이 있다면 타입까지 고려해주어야한다.)
Address address = new Address("서울시", "강남구");
Person person= createInstanceWithArguments(Person.class, address, "홍길동", 25);
/*
// 결과 :
Person{address=Address{city='서울시', street='강남구'}, name='홍길동', age=25}
*/
// ===================================================================================
Person person= createInstanceWithArguments(Person.class, "홍길동", 25);
/*
// 결과 :
Person{address=null, name='홍길동', age=25}
*/
Constructor.setAccessible(true): package-private 클래스 생성
public으로 선언되지 않은 클래스는 package-private 클래스로 해당 패키지 클래스에서만 볼 수 있게된다.
외부 API를 나타내는 클래스만 public을 설정함으로써 패키지 사용자가 어떤 클래스를 사용해야하는지 혼란스럽지 않고 내부 데이터 모델, 헬퍼 클래스 구현 상세 사항에 대해 헷갈리지 않는다.
개발을 진행하다보면 private-package 클래스도 패키지 외부에서 꼭 접근해야하는 경우가 있다.
예를 들어 클래스 분석이나 인스턴스화를 위해 외부 라이브러리를 사용해야할 때 접근해야한다. 외부 라이브러리 코드는 패키지 밖에 있으므로 Reflection 없이는 내부 package-private 클래스에 접근할 수 없다.
public Object createClassInstace(Constructor<?> packagePrivateClassCtor, Object args) {
packagePrivateClassCtor.setAccessible(true);
return packagePrivateClassCtor.newInstance(args);
}
이럴 때, newInstance 메서드를 호출 하기 전에 setAccessible을 true로 설정 하여 private-package 클래스의 인스턴스를 생성할 수 있다.
Json 데이터를 생성하기 위해 자주 사용하는 ObjectMapper 라이브러리를 생각해보자.
byte[] reqData = objectMapper.writeValueAsBytes(request); // 요청 객체
byte[] resData = send(reqData); // 요청 후 응답
Response response = objectMapper.readValue(resData, Response.class); // 역직렬화
외부 라이브러리 ObjectMapper는 Reflection을 이용해 request 객체를 분석해 요청 객체를 만든다.
해당 데이터로 요청해 받은 데이터는 byte로 표현되어 있기에 또 다시 ObjectMapper를 이용해 해당 데이터를 Java 객체로 역직렬화 한다.
이러한 것이 가능한 이유는 Java Reflection이 클래스를 반영하고 인스턴스화하는 기능을 갖췄기 때문이다.
'Language > Java' 카테고리의 다른 글
Java native 키워드로 Rust코드와 연동하기 (2) | 2024.09.19 |
---|---|
Java Modifier(제어자) (1) | 2024.09.17 |
Java Reflection(3) (1) | 2024.09.15 |
Java - unchecked casting (1) | 2024.09.13 |
Java Reflection(2) (0) | 2024.08.25 |