본문 바로가기

Language/Java

Java Reflection(1)

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