리플렉션(Reflection)이란?
리플렉션(Reflection)은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API이다.
컴파일 시간이 아닌 실행시간에 동적으로 클래스의 정보를 추출할 수 있는 프로그래밍 기법이다.
리플렉션은 동적으로 클래스를 사용해야 할 때 필요하다.
즉, 작성 시점에는 어떠한 클래스를 사용해야 할지 모르지만 런타임 시점에서 클래스를 가져와서 실행해야 하는 경우 필요하다.
클래스 파일의 위치나 이름만 있다면 해당 클래스의 정보를 얻어내고, 객체를 생성하는 것 또한 가능하게 해주어 유연한 프로그래밍을 가능케 해 준다.
리플렉션은 애플리케이션 개발에서보다는 프레임워크, 라이브러리에서 많이 사용된다.왜냐하면 프레임워크, 라이브러리는 사용하는 사람이 어떤 클래스명과 멤버들을 구성할지 모르는데, 이러한 사용자 클래스들을 기존의 기능과 동적으로 연결시키기 위하여 리플렉션을 사용한다고 보면 된다.
대표적으로 Intellij의 자동완성 기능과 Spring 프레임워크의 DI, Proxy, ModelMapper 등이 있다.
이미 Spring, Hibernate, Lombok 등 많은 프레임워크에서 Reflection 기능을 사용하고 있다.
리플렉션은 아래와 같은 정보를 가져올 수 있다.
- Class
- Constructor
- Method
- Field
Class 클래스
우리는 보통 자바로 프로그래밍을 할 때 변수나 클래스를 직접 선언하고 만들어서 사용한다.
하지만 어떤 경우에는 애플리케이션 실행 중에 동적으로 불러와서 다루어야 하는 경우가 생긴다.
즉, 코드를 실행하기 전 컴파일 단에서 직접 클래스 정의문을 찾아 클래스 정보를 얻는 것이 아니라, 코드 상에서 호출 로직을 통해 클래스 정보를 얻어와 다룸으로써 런타임 단에서 다이나믹하게 클래스를 핸들링할 수 있다.
이때 사용되는 것이 바로 Class 클래스 객체다.
Class 클래스는 java.lang.Class
패키지에 별도로 존재하는 독립형 클래스로서, 자신이 속한 클래스의 모든 멤버 정보를 담고 있기 때문에 런타임 환경에서 동적으로 저장된 클래스나 인터페이스 정보를 가져오는 데 사용된다.
자바의 모든 클래스와 인터페이스는 컴파일 과정을 통해 .java
-> .class
파일로 변환된다.
이 .class
파일에는 멤버 변수, 메서드, 생성자 등의 객체 정보가 들어 있다.
JVM의 클래스 로더(ClassLoader)에 의해서 클래스 파일이 메모리에 올라갈 때, Class 클래스는 이 .class
파일의 클래스 정보들을 가져와 힙 영역에 자동으로 객체화가 되게 된다. 그래서 따로 new
인스턴스화 없이 바로 가져와 사용하면 된다.
.java
파일을 컴파일 하게되면 아래와 같은 구조의 .class
파일이 생성된다.
Class 클래스 객체 얻기
Class 객체를 가져오는 방법은 다음의 3가지가 있다.
- Object.getClass()
- .class 리터럴
- Class.forName()
Object.getClass()
- 모든 클래스의 최상위 클래스인 Object 클래스에서 제공하는
getClass()
메서드를 사용해 가져온다. - 해당 클래스가 인스턴스화된 상태 이어야 한다는 제약이 있다.
public static void main(String[] args) {
// 스트링 클래스 인스턴스화
String str = new String("Class Test");
// getClass() 메서드로 얻기
Class<? extends String> cls = str.getClass();
System.out.println(cls); // class java.lang.String
}
.class 리터럴로 얻기
- 인스턴스가 존재하지 않고, 컴파일된 클래스 파일만 있다면 리터럴로 Class 객체를 곧바로 얻을 수 있다.
public static void main(String[] args) {
// 클래스 리터럴(*.class)로 얻기
Class<? extends String> cls = String.class;
System.out.println(cls); // class java.lang.String
}
Class.forName()으로 얻기
- 리터럴 방식과 같이 컴파일된 클래스 파일이 있다면 클래스 이름만으로 Class 객체를 반환받을 수 있다.
- 단, 이때는 클래스의 도메인을 상세히 적어주어야 한다. 그래서 클래스 파일 경로에 오타가 있으면 에러가 발생할 수 있기 때문에 주의해야 한다.
- Class 객체를 찾지 못하면
ClassNotFoundException
를 발생시키기 때문에 예외처리가 강제된다. - 그러나 다른 두 가지 방법보다 forName을 통해 얻게 되면 메모리를 절약하며 런타임 동적 로딩을 할 수 있기 때문에 가장 성능이 좋다. (클래스 로더와 런타임 동적 로딩)
public static void main(String[] args) {
try {
// 도메인.클래스명으로 얻기
Class<?> cls = Class.forName("java.lang.String");
System.out.println(cls); // class java.lang.String
} catch (ClassNotFoundException e) {}
}
동적으로 생성자 가져와 초기화하기
Person 클래스에는 public과 private, static한 필드와 메소드를 가지고 있고, 생성자도 매개 변수에 따라 두 가지로 준비했다.
class Person {
public String name; // public 필드
private int age; // private 필드
public static int height = 180; // static 필드
// 이름, 나이를 입력받는 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 기본 생성자
public Person() {
}
public void getField() {
System.out.printf("이름 : %s, 나이 : %d\n", name, age);
}
// public 메소드
public int sum(int left, int right) {
return left + right;
}
// static 메소드
public static int staticSum(int left, int right) {
return left + right;
}
// private 메소드
private int privateSum(int left, int right) {
return left + right;
}
}
이제 동적으로 생성자를 가져와 초기화해보자.
getConstructor()
를 호출할 때 인자로 생성자의 매개변수 타입을 바인딩해주어야 한다.- 만일 어떠한 매개변수 타입을 지정해주지 않으면 기본생성자가 호출되게 된다.
- 만약 해당하는 생성자를 찾지 못하면
NoSuchMethodException
이 발생된다.
public static void main(String[] args) throws Exception {
// 클래스 객체 가져오기 (forName 메소드 방식)
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// 생성자 가져오기 - Person(String name, int age)
Constructor<Person> constructor = personClass.getConstructor(String.class, int.class); // getConstructor 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.
// 가져온 생성자로 인스턴스 만들기
Person person1 = constructor.newInstance("홍길동", 55);
person1.getField(); // 이름 : 홍길동, 나이 : 55
}
동적으로 메서드 가져와 실행하기
getMethod()
를 호출할때 인자로 생성자의 매개변수 타입을 바인딩 해주어야 한다.- 만약 매개변수가 없는 메소드라면 메소드 명만 입력해주면 된다.
- 실행은 Method 타입에서 제공하는
invoke()
를 호출하여 실행하면 된다.- instance 메소드 - 매개변수로 인스턴스 필요
- static 메소드 - 매개변수 필요 없음
- private 메소드 - invoke 하기 전에 공개화할 필요 있음
public static void main(String[] args) throws Exception {
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// 특정 public 메서드 가져와 실행
// getMethod("메서드명", 매개변수타입들)
Method sum = personClass.getMethod("sum", int.class, int.class);
int result = (int) sum.invoke(new Person(), 10, 20);
System.out.println("result = " + result); // 30
// 특정 static 메서드 가져와 실행
Method staticSum = personClass.getMethod("staticSum", int.class, int.class);
int staticResult = (int) staticSum.invoke(null, 100, 200);
System.out.println("staticResult = " + staticResult); // 300
// 특정 private 메서드 가져와 실행
Method privateSum = personClass.getDeclaredMethod("privateSum", int.class, int.class);
privateSum.setAccessible(true); // private 이기 때문에 외부에서 access 할 수 있도록 설정
int privateResult = (int) privateSum.invoke(new Person(), 1000, 2000);
System.out.println("privateResult = " + privateResult); // 3000
}
동적으로 필드 가져와 조작하기
getField()
를 통해 클래스의 필드를 얻을 수 있다.- 필드 값 변경은
set()
메서드를 호출하면 된다. - 필드는 클래스가 인스턴스가 되어야 Heap 메모리에 적재됨으로 인스턴스가 필요하다.
- 다만, static 필드라면 Method Area에 이미 적재되어 있으므로 인스턴스가 필요 없다.
public static void main(String[] args) throws Exception {
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// static 필드를 가져와 조작하고 출력하기
Field height_field = personClass.getField("height");
height_field.set(null, 200);
System.out.println(height_field.get(null)); // 200
}
public static void main(String[] args) throws Exception {
Person person = new Person("홍길동", 55);
// 클래스 객체 가져오기
Class<Person> personClass = (Class<Person>) Class.forName("Person");
// public 필드를 가져온다.
Field name_field = personClass.getField("name");
// private 필드를 가져온다.
Field age_field = personClass.getDeclaredField("age");
age_field.setAccessible(true); // private 이기 때문에 외부에서 access 할 수 있도록 설정
// 필드 조작하기
name_field.set(person, "임꺽정");
age_field.set(person, 88);
System.out.println(name_field.get(person)); // 임꺽정
System.out.println(age_field.get(person)); // 200
}
리플렉션의 단점
일반적으로 메소드를 호출한다면, 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임에 클래스를 분석하므로 속도가 느리다.
그리고 이런 특징으로 타입 체크가 컴파일 타임에 불가능하다. 또한 객체의 추상화가 깨진다는 단점도 존재한다.
따라서 정말 필요한 곳에만 리플렉션을 한정적으로 사용해야 한다.
컴파일 시점에 오류를 잡을 수 없기 때문에 getMethod("callA")
안에 들어가는 문자를 실수로 getMethod("callZ")
로 작성해도 컴파일 오류가 발생하지 않는다. 그러나 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임 오류가 발생한다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
참고 자료 :
https://www.viralpatel.net/tutorial-java-class-file-format-revealed/
'백엔드 > Java' 카테고리의 다른 글
[Java] Collection API 개선 (0) | 2023.06.23 |
---|---|
[Java] 병렬 스트림 (parallel stream) (0) | 2023.06.22 |
[Java] 프록시 패턴(Proxy Pattern) (0) | 2023.05.24 |
[Java] JVM의 Class Loader (0) | 2023.05.23 |
[Java] 상속(Inheritance)과 합성(Composition) (1) | 2023.05.19 |