리플렉션 (Reflection) 이란?
리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있는 자바 API 를 말한다.
자바의 리플렉션은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경하거나 메소드를 호출할 수 있다.
리플렉션은 언제 사용할까?
- 동적으로 클래스를 사용해야 하는 경우
- 코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우
- 프레임워크나 IDE 에서 이런 동적 바인딩을 이용한 기능을 제공한다. IntelliJ 의 자동완성 기능, 스프링의 어노테이션이 리플렉션을 이용한 기응이다.
리플렉션을 사용하여 가져올 수 있는 정보는 다음과 같다.
- Class
- Constructor
- Method
- Field
JVM 은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장한다.
그렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, 리플렉션이라는 이름을 가지게 되었다.
리플렉션을 사용하면 생성자, 메소드, 필드 등 클래스에 대한 정보를 아주 자세히 알아낼 수 있다.
대표적으로 여러 라이브러리, 프레임워크에서 사용되는 어노테이션이 리플렉션을 사용한 예시이다.
어노테이션은 그 자체로는 아무 역할도 하지 않는다. 리플렉션 덕분에 우리가 스프링에서 @Component, @Bean 과 같은 어노테이션을 프레임워크의 기능을 사용하기 위해 사용할 수 있는 것이다.
또한, 인텔리제이와 같은 IDE 에서 Getter, Setter 를 자동으로 생성해주는 기능도 리플렉션을 사용하여 필드 정보를 가져와 구현한다고 한다. 이와 같이 리플렉션은 다양한 곳에서 무궁무진한 방법으로 사용될 수 있다.
리플렉션을 사용하면 접근 제어자와 무관하게 클래스의 필드나 메소드도 가져와서 호출할 수 있다는 점이다.
Class
리플렉션의 가장 핵심은 Class 이다. Class 는 java.lang 패키지에서 제공된다. 어떻게 특정 클래스의 Class 인스턴스를 획득할 수 있을까?
Class 객체 획득 방법
Class<Member> aClass = Member.class; // (1)
Member member1 = new Member();
Class<? extends Member> bClass = member1.getClass(); // (2)
Class<?> cClass = Class.forName("yoni.reflection.test"); // (3)
첫번째 방법으로는 클래스의 class 프로퍼티를 통해 회득하는 방법이다.
두번째 방법으로는 인스턴스의 getClass() 메소드를 사용하는 방법이다.
세번째 방법으로는 Class 클래스 의 forName() 정적 메소드에 FQCN(Fully Qualified Class Name) 을 전달하여 해당 경로와 대응하는 클래스에 대한 Class 클래스 의 인스턴스를 얻는 방법이다.
이런 Class 의 객체는 Class 에 public 생성자가 존재하지 않아 우리가 직접 생성할 수 있는 방법은 없다. 대신 Class 의 객체는 JVM 이 자동으로 생성해준다.
getXXX() vs getDelcaredXXX()
Class 객체의 메소드 중
getFields(), getMethods(), getAnnotations() 와 같은 형태와
getDeclaredFields(), getDeclaredMethods(), getDeclaredAnnotations() 와 같은 형태로 메소드가 정의되어 있는 것을 확인할 수 있다. 이 메소드들은 클래스에 정의된 필드, 메소드, 어노테이션 목록을 가져오기 위해 사용된다.
이 둘은 무엇이 다른걸까?
getXXX() 는 상속받은 클래스와 인터페이스를 포함하여 모든 public 요소를 가져온다. 예를 들면 getMethods() 는 해당 클래스가 상속받은, 그리고 구현한 인터페이스에 대한 모든 public 메소드를 가져온다.
반면, getDeclaredXXX() 는 상속받은 클래스와 인터페이스를 제외하고 해당 클래스에 직접 정의된 내용만 가져온다. 또한 접근 제어자와 상관없이 요소에 접근할 . 수있다. 예를 들어 getDeclaredMethods() 는 해당 클레스에만 직접 정의된 private, protected, public 메소드를 전부 가져온다.
Constructor
Class 를 사용해서 생성자를 Constructor 타입으로 가져올 수 있다.
Constructor 는 java.lang.reflect 패키지에서 제공하는 클래스이며, 클래스 생성자에 대한 정보와 접근을 제공한다.
리플렉션으로 생성자에 직접 접근하고, 객체를 생성해보자.
// 생성자 가져오기
Class aClass = Class.forName("yoni.reflection.test")
Constructor constructor = aClass.getDeclaredConstructor();
위와 같이 Class 타입 객체의 getDeclaredConstructor() 를 사용하여 인자 없는 Constructor 를 획득할 수 있다.
그렇다면, 인자가 있는 오버로딩 생성자를 가져오는 방법은 무엇일까? 간단하다. 타입에 일치하는 인자를 넣어주면 된다.
Class aClass = Class.forName("yoni.reflection.test")
Constructor noArgsConstructor = aClass.getDeclaredConstructor();
Constructor onlyNameConstructor = aClass.getDeclaredConstructor(String.class);
Constructor allArgsConstructor = aClass.getDeclaredConstructor(String.class, int.class);
public, private 등 모든 생성자를 가져오는 방법이 있고,
Class aClass = Class.forName("yoni.reflection.test")
Constructor constructors[] = aClass.getDeclaredConstructor();
for (Constructor cons : constructors) {
System.out.println("Get constructors in Child: " + cons);
}
// 출력
// Get constructors in Child: private yoni.reflection.test(java.lang.String)
// Get constructors in Child: public yoni.reflection.test()
public 생성자만 가져오는 방법도 있다.
Class aClass = Class.forName("yoni.reflection.test")
Constructor constructors[] = aClass.getConstructors();
for (Constructor cons : constructors) {
System.out.println("Get public constructors in Child: " + cons);
}
// 출력
// Get public constructors in both Parent and Child: public yoni.reflection.test()
Method
리플렉션을 사용하여 Method 타입의 오브젝트를 획득하여 객체 메소드에 직접 접근할 수 있다.
Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);
Method sayMyName = aClass.getDeclaredMethod("sayMyName");
sayMyName.invoke(member);
// 출력
// 내 이름은 요니
위와 같이 Method 타입의 invoke() 를 사용하여 메소드를 직접 호출할 수도 있다.
Field
리플렉션을 사용하여 Field 타입의 오브젝트를 획득하여 객체 필드에 직접 접근할 수 있다.
Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);
for (Field field : aClass.getDeclaredFields()) {
field.setAccessible(true);
String fieldInfo = field.getType() + ", " + field.getName() + " = " + field.get(member);
System.out.println(fieldInfo);
}
// 출력
// class java.lang.String, name = 요니
// int, age = 20
아래와 같이 set() 메소드를 사용하여 Setter 없이도 강제로 객체의 필드 값을 변경할 수도 있다.
Class<Member> aClass = Member.class;
Member member = new Member("요니", 20);
Field name = aClass.getDeclaredField("name");
name.setAccessible(true);
name.set(member, "Yoni"); // 필드값 변경
System.out.println("member = " + member);
// 출력
// member = Member{name='Yoni', age=20}
어노테이션 가져오기
Class<Member> aClass = Member.class;
Entitiy entityAnnotation = aClass.getAnnotation(Entitiy.class);
String value = entityAnnotation.value();
System.out.println("value = " + value);
// 멤버
위와 같이 getAnnotation() 메소드에 직접 어노테이션 타입을 넣어주면, 클래스에 붙어있는 어노테이션을 가져올 수 있다.
어노테이션이 가지고 있는 필드에도 접근할 수 있는 것을 확인할 수 있다.
리플렉션의 단점
일반적으로 메소드를 호출한다면, 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임에 클래스를 분석하므로 속도가 느리다.
JVM 을 최적화할 수 없기 때문이라고 한다. 그리고 이런 특징으로 인해 타입 체크가 컴파일 타임에 불가능하다. 또한 객체의 추상화가 깨진다는 단점도 존재한다.
따라서 일반적인 웹 애플리케이션 개발자는 리플렉션을 사용할 일이 거의 없다. 보통 라이브러리나 프레임워크를 개발할 때 사용된다.
따라서 정말 필요한 곳에서만 리플렉션을 사용하도록 하자.
References
'JAVA' 카테고리의 다른 글
[OOP] 객체지향 설계의 5가지 원칙 (SOLID) (1) | 2023.12.22 |
---|---|
[JAVA] Garbage Collection(가비지 컬렉션) 알고리즘 종류 (0) | 2023.12.22 |
[JAVA] JVM 내부 구조 & 메모리 영역 (2) | 2023.12.21 |