리플렉션은 단어의 뜻 처럼 클래스의 멤버(필드, 생성자, 메서드), 어노테이션, 상위 클래스 등 클래스에 대한 정보를 반사 하듯 확인할 수 있는 자바 API입니다. 정보를 확인하는 것 뿐만 아니라 직접 인스턴스를 생성 및 조작하는 등, 클래스를 런타임에 동적으로 다룰 수 있습니다. 보통 구체적인 클래스 타입을 알지 못할 때 사용합니다. 일반적으로 개발자가 애플리케이션을 만들 때 리플렉션을 다룰 일은 많이 없지만, Spring Framework, jackson 등의 프레임워크나 라이브러리에서 많이 사용 되기 때문에 깊게 이해하기 위해선 리플렉션에 대한 학습이 필수라 생각합니다.
스프링 내에서 리플렉션을 사용하는 예 중 하나는 IoC 컨테이너입니다. 클래스에 @Component 어노테이션을 선언하기만 하면 컨테이너가 자동으로 @Autowired가 선언된 필드에 주입을 해주는 데 이 때 리플렉션을 사용합니다. 스프링 애플리케이션 구동시 @Component 어노테이션(또는 @Service, @Bean 등)이 붙은 클래스 들을 불러온 후 인스턴스를 생성하여 컨테이너에 저장하고, @Autowired가 붙은 필드에 주입을 해 줍니다(생성자 주입, Setter 주입도 마찬가지입니다). 그럼 도대체 어떤 방식으로 인스턴스를 생성하고 관리하는 것일까요? 일단 리플렉션의 사용 법을 알아본 후 간단하게 IoC 컨테이너를 흉내내 보도록 하겠습니다.
Class 인스턴스 생성하기
리플렉션은 Class 인스턴스를 통해 사용할 수 있는데, 이 인스턴스를 통해 Constructor, Method, Field 등의 인스턴스를 다시 가져와서 정보를 확인 하거나 호출하여 반환 값을 얻을 수 있습니다. Class 인스턴스를 생성하기 위해서는 다양한 방법이 있는데요. Class 클래스의 스태틱 메서드 호출, 인스턴스의 getClass 함수 호출, 클래스 뒤에 .class 호출 이렇게 세 가지입니다.
// 1. 객체를 통해 직접 인스턴스 가져온다.
Class<MyClass> clazz1 = MyClass.class;// 2. 인스턴스를 통해 가져온다
MyClass myClass = new MyClass();
Class<? extends MyClass> clazz2 = myClass.getClass();// 3. 패키지 경로 + 클래스 명을 통해 인스턴스를 가져온다.
Class<?> clazz2 = Class.forName("com.test.MyClass");
첫 번째 경우는 직접 클래스를 통해 가지고 오기 때문에 제네릭 타입을 클래스타입으로 추론 가능하지만, 두 번째 경우에는 인스턴스를 통해 가져오기 때문에 상위타입 제한(? extends 상위타입)으로 가져올 수 있습니다. 마지막으로 세 번째는 문자열을 인자로 받기 때문에 추론이 불가하여 와일드카드(?)로 받아 오게 됩니다. 하지만 제네릭 타입을 정확하게 추론한다 하더라도 Class 인스턴스에 있는 메서드를 사용하기 때문에 사용시 차이는 거의 없습니다. 여러가지 방법이 있기 때문에 상황에 맞게 사용하는 것이 중요합니다. 다만 마지막 방법은 존재 하지 않는 클래스를 찾으면 ClassNotFoundException이 발생하기 때문에 주의가 필요합니다.
Class 클래스의 메서드 들
이제 Class 클래스에 있는 몇가지 메서드를 살펴보겠습니다. 필드, 메서드, 생성자 등을 가져올 수 있습니다.
- getFields: public 필드 배열을 가지고 온다.
- getDeclaredFields: 모든 필드 배열을 가지고 온다.
- getConstructors: public 생성자 배열을 가지고 온다.
- getDeclaredConstructors: 모든 생성자 배열을가져온다
- getMethods: public 메서드 배열을 가지고 온다.
- getDeclaredMethod: 모든 메서드 배열을 가지고 온다.
- …
위에 나열된 메서드 이외에 상위 클래스나 어노테이션의 정보 등, 클래스에 대한 모든 정보를 확인할 수 있습니다. 위에 나열된 메서드 이외에 다른 메서드가 궁금하다면 레퍼런스를 참고해 보시길 바랍니다.
해당 객체의 인스턴스 만들기
이제 Class 인스턴스를 가지고 왔으니 인스턴스를 생성해보겠습니다. 위에서 봤던 getContstructors 메서드를 통해 public 생성자를 모두 가져올 수 있습니다.
Constructor[] constructors = clazz.getContructors();
생성자 배열을 가지고 왔으니 newInstance 메서드를 호출하여 인스턴스를 만들 수 있습니다. 하지만 현재는 모든 생성자를 가지고 왔으니 0번째 요소를 가져와서 생성해줍시다. 생성자에 파라미터가 있다면 함께 넣어 줍니다.
class MyClass {
private String a;
private int b; public MyClass() {} public MyClass(String a) {
this.a = a;
} private MyClass(int b) {
this.b = b;
}
}
...
...Class<?> clazz = Class.forName("MyClass");Constructor[] constructors = clazz.getConstructors();//기본 생성자 호출
Object obj1 = constructors[0].newInstance();//파라미터가 있는 생성자 호출
Object obj2 = constructors[1].newInstance("str");constructors[2].newInstance(1); // ArrayIndexOutOfBoundsException
첫 번째는 기본 생성자이므로 파라미터가 없습니다. 그렇기 때문에 newInstance 메서드에 파라미터를 넣지 않고 호출합니다. 두 번째는 String 타입의 파라미터를 받기 때문에 String을 파라미터로 넣고 호출합니다. 스프링에서 생성자 주입도 이와 마찬가지로 이루어 집니다. 마지막으로 세 번째는 private 생성자이기 때문에 가져오지 않습니다. 그렇기 때문에 ArrayIndexOutOfBoundException이 발생하게 됩니다.
여기까지 리플렉션을 이용해 인스턴스를 만들어 보았습니다. 만약 private 생성자를 가져오고 싶다면 getConstructors 대신 getDeclaredConstructors를 호출해야 하겠지요. 물론 private이기 때문에 바로 호출 할 순 없고 추가 설정이 필요하게 됩니다. 다음 글에서 메서드 호출과 필드 접근을 실습해 보며 알아보겠습니다.