# 1 리플렉션이란
자바 클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능을 리플렉션이라 합니다.
리플렉션을 통해 프로그램 실행 중에 클래스, 메소드, 필드 등에 대한 정보를 얻거나, 새로운 객체를 생성하고 메서드를 호출하며, 필드의 값을 읽고 쓸 수 있습니다.
리플렉션을 통해 얻을 수 있는 정보는 다음과 같습니다.
- 클래스의 메타데이터 : 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스등
- 필드 정보 : 필드의 이름, 타입, 접근 제어자를 확인하고, 해당 필드의 값을 읽거나 수정할 수 있다.
- 메서드 정보 : 메서드 이름, 반환 타입, 매개변수 정보를 확인하고, 실행 중에 동적으로 메서드를 호출 할 수 있다.
- 생성자 정보 : 생성자의 매개변수 타입과 개수를 확인하고, 동적으로 객체를 생성할 수 있다.
# 2 왜 사용하는가..?
보통 일반적인 로직을 작성할때 사용하진 않는다 하지만, 라이브러리 프레임워크에서는 자바의 리플렉션을 활용한 기능들이 굉장히 많습니다.
예를 들어 자바의 Junit, 어노테이션 (메타데이터) 를 읽고 처리할 수 있습니다.
자바에서 가장 많이 사랑받는 프레임워크인 Spring 도 에노테이션 기법으로 많은 기능을 구현했는데 이 역시 자바의 리플렉션을 활용한 기능이라고 볼 수 있습니다.
컨트롤러 예시 1
public class Controller {
@GetMapping("/site/name")
public String goPage() {
// 필요한 로직 작성..
return "sitePage";
}
}
아주 흔한 GetMapping 에노테이션을 볼 수 있다.
하지만 에노테이션이 정확히 어떤 방식으로 동작하는지는 정확하게 알 수 없이 편하게 사용해왔다면
이제는 리플렉션이라는 개념과 동작방식에 대해 알 필요가 있다고 생각합니다.
# 3 클래스 메타데이터 조회
먼저 리플렉션을 통해 클래스를 조회 하는 3가지 방법이 존재한다
- 클래스에서 찾기
- 인스턴스에서 찾기
- 문자로 찾기
위 3가지를 코드 예제를 통해 알아봅시다.
package reflection;
import reflection.data.BasicData;
public class BasicV1 {
public static void main(String[] args) throws ClassNotFoundException {
// 클래스 메타데이터 조회 방법 3가지
// 1. 클래스에서 찾기
Class<BasicData> basicDataClass1 = BasicData.class;
System.out.println("basicDataClass1 = " + basicDataClass1);
// 2. 인스턴스에서 찾기
BasicData basicInstance = new BasicData();
Class<? extends BasicData> basicDataClass2 = basicInstance.getClass();
System.out.println("basicDataClass2 = " + basicDataClass2);
// 3. 문자로 찾기
String className = "reflection.data.BasicData"; // 패키지명 주의
Class<?> basicDataClass3 = Class.forName(className);
System.out.println("basicDataClass3 = " + basicDataClass3);
}
}
이제 이렇게 구한 클래스 정보를 통해서 할 수 있는 조작은 다음과 같습니다.
- 기본동작탐색
- 메서드 탐색과 동적 호출
- 필드 탐색과 값 변경
위 3가지에 대해서 예제 코드를 보면서 하나씩 이해 해봅시다.
위 3가지에 대해 이해를 한다면 스프링 프레임워크 혹은 롬복같은 라이브러리가 어떻게 동작을 했는지 알 수 있을것이다.
혹은 더 편하고 안전한 롬복 라이브러리를 개발 할 수 도 있다. (좀더 스레드 세이프 하게 작성을 하거나 혹은 특정 객체를 불변으로 만들어주는 그런 라이브러리 말이다)
# 4 기본동작탐색
#3을 통해 우리는 클래스의 메타정보를 가져오는 방법을 알게되었는데
이렇게 찾은 클래스 메타데이러로 어떤일을 할 수 있는지 알아보겠습니다.
package reflection;
import reflection.data.BasicData;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class BasicV2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<BasicData> basicData = BasicData.class;
System.out.println("basicData.getName() = " + basicData.getName());
System.out.println("basicData.getSimpleName() = " + basicData.getSimpleName());
System.out.println("basicData.getPackage() = " + basicData.getPackage());
System.out.println("basicData.getSuperclass() = " + basicData.getSuperclass());
System.out.println("basicData.getInterfaces() = " + Arrays.toString(basicData.getInterfaces()));
System.out.println("basicData.isInterface() = " + basicData.isInterface());
System.out.println("basicData.isEnum() = " + basicData.isEnum());
System.out.println("basicData.isAnnotation() = " + basicData.isAnnotation());
int modifiers = basicData.getModifiers();
System.out.println("basicData.getModifiers() = " + modifiers);
System.out.println("isPublic = " + Modifier.isPublic(modifiers));
System.out.println("Modifier.toString() = " + Modifier.toString(modifiers));
}
}
실행결과
basicData.getName() = reflection.data.BasicData
basicData.getSimpleName() = BasicData
basicData.getPackage() = package reflection.data
basicData.getSuperclass() = class java.lang.Object
basicData.getInterfaces() = []
basicData.isInterface() = false
basicData.isEnum() = false
basicData.isAnnotation() = false
basicData.getModifiers() = 1
isPublic = true
Modifier.toString() = public
```
클래스 이름, 패키지, 부모 클래스, 구현한 인터페이스, 수정자 정보등 다양한 정보를 획득할 수 있다.
참고로 수정자는 접근 제어자와 비 접근 제어자(기타 수정자)로 나눌 수 있다.
- 접근 제어자: `public` , `protected` , `default` ( `package-private` ), `private`
- 비 접근 제어자: `static` , `final` , `abstract` , `synchronized` , `volatile` 등
# 4 -1 메서드 탐색과 동적 호출
클래스 메타데이터를 통해 클래스가 제공하는 메서드의 정보를 확인할 수 있습니다.
package reflection;
import reflection.data.BasicData;
import java.lang.reflect.Method;
public class MethodV1 {
public static void main(String[] args) {
Class<BasicData> helloClass = BasicData.class;
System.out.println("====== methods() =====");
Method[] methods = helloClass.getMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
System.out.println("====== declaredMethods() =====");
Method[] declaredMethods = helloClass.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println("declaredMethod = " + method);
}
}
}
- Class.getMethod() 또는 Class.getDeclaredMethods()를 호출하면 Method 라는 메서드의 메타 데이터를 얻을 수 있다. 이 클래스는 메서드의 모든 정보를 가지고 있다.
getMethod() vs getDeclaredMethods()
- getMethod() : 해당 클래스의 상위 클래스에서 상속된 모든 public 메서드를 반환
- getDeclaredMethods() : 해당 클래스에서 선언된 모든 메서드를 반환하며, 접근 제어자에 관계없이 반환. 상속된 메서드는 포함하지 않음
실행결과
====== methods() =====
method = public void reflection.data.BasicData.call()
method = public java.lang.String
reflection.data.BasicData.hello(java.lang.String)
method = public final void java.lang.Object.wait(long,int) throws
java.lang.InterruptedException
method = public final void java.lang.Object.wait() throws
java.lang.InterruptedException
method = public final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException
method = public boolean java.lang.Object.equals(java.lang.Object)
method = public java.lang.String java.lang.Object.toString()
method = public native int java.lang.Object.hashCode()
method = public final native java.lang.Class java.lang.Object.getClass()
method = public final native void java.lang.Object.notify()
method = public final native void java.lang.Object.notifyAll()
====== declaredMethods() =====
declaredMethod = public void reflection.data.BasicData.call()
declaredMethod = private void reflection.data.BasicData.privateMethod()
declaredMethod = void reflection.data.BasicData.defaultMethod()
declaredMethod = protected void reflection.data.BasicData.protectedMethod()
declaredMethod = public java.lang.String
reflection.data.BasicData.hello(java.lang.String)
# 4 -2 메서드 탐색과 동적 호출
Method 객체를 사용해서 메서드를 직접 호출 할 수 도 있다.
package reflection;
import reflection.data.BasicData;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MethodV2 {
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 정적 메서드 호출 - 일반적인 메서드 호출
BasicData helloInstance = new BasicData();
helloInstance.call(); // 이 부분은 코드를 변경하지 않는 이상 정적이다.
// 동적 메서드 호출 - 리플렉션 사용
Class<? extends BasicData> helloClass = helloInstance.getClass();
String methodName = "hello";
// 메서드 이름을 변수로 변경할 수 있다.
Method method1 = helloClass.getDeclaredMethod(methodName, String.class);
Object returnValue = method1.invoke(helloInstance, "hi");
System.out.println("returnValue = " + returnValue);
}
}
실행결과
BasicData.BasicData
BasicData.call
BasicData.hello
returnValue = hi hello
# 4 -3 필드 탐색과 값 변경
리플렉션을 활용해서 필드를 탐색하고 또 필드의 값을 변경하도록 활용할 수 있다.
필드탐색
package reflection;
import reflection.data.BasicData;
import java.lang.reflect.Field;
public class FieldV1 {
public static void main(String[] args) {
Class<BasicData> helloClass = BasicData.class;
System.out.println("====== fields() =====");
Field[] fields = helloClass.getFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
System.out.println("====== declaredFields() =====");
Field[] declaredFields = helloClass.getDeclaredFields();
for (Field field : declaredFields) {
System.out.println("declaredField = " + field);
}
}
}
실행결과
====== fields() =====
field = public java.lang.String reflection.data.BasicData.publicField
====== declaredFields() =====
declaredField = public java.lang.String reflection.data.BasicData.publicField
declaredField = private int reflection.data.BasicData.privateField
fields() vs declaredFields()
앞서 설명한 getMehtod() vs getDeclaredMethods() 와 같은 개념으로 볼 수 있다.
- fields(): 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환
- declaredFields() : 해당 클래스에서 선언된 모든 필드를 반환하며, 접근 제어자에 관계없이 반환, 상속된 필드는 포함하지 않음
필드값 변경
필드 데이터 변경을 위한 간단한 예제 클래스
package reflection.data;
public class User {
private String id;
private String name;
private Integer age;
public User() {
}
public User(String id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
필드 변경 코드
package reflection;
import reflection.data.User;
import java.lang.reflect.Field;
public class FieldV2 {
public static void main(String[] args) throws Exception {
User user = new User("id1", "userA", 20);
System.out.println("기존 이름 = " + user.getName());
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name");
// private 필드에 접근 허용, private 메서드도 이렇게 호출 가능
nameField.setAccessible(true);
nameField.set(user, "userB");
System.out.println("변경된 이름 = " + user.getName());
}
}
여기서 주의할점은 User 클래스 필드 접근제어가자 private 임을 주의해야하는데
사실 private 접근제어자에 접근은 불가능하지만
리플레션에서는 private 필드에 접근할 수 있는 특별한 기능을 제공한다.
nameField.setAccessible(true)
참고로 SetAccessible(true) 기능은 Method도 제공한다. 따라서 private 메서드 마찬가지로 호출할 수 있다.
리플레션 주의사항
리플렉션을 사용하면 Private 접근제어자도 직접 접근해서 값을 변경할 수 있다. 하지만 이는 객체 지향 프로그램밍의 원칙을 위반하는 행위로 간주될 수 있다. private 접근제어자는 클래스 내부에서만 데이터를 보호하고, 외부에서의 직접적인 접근을 방지하기 위해서 사용한다
(특히 스레드에서 사용해야하는 객체라 불변객체로 설계를 했지만 의도를 파악하지 못하고 private 필드에 접근하여 스레드 안정성을 해칠 수 있는 위험성도 존재한다. )
따라서 리플렉션을 사용할 때는 반드시 신중하게 접근해야 하며, 가능한 경우 접근 메서드 (getter, setter)를 사용하는 것이 바람직하다.
리플렉션은 주로 테스트 라이브러리나 개발 같은 특별한 상황에서 유용하게 사용되지만, 일반적인 애플리케이션 코드에서는 권장되지 않는다.
리플렉션을 활용 예
1. 서블릿 컨테이너 : 요청을 처리할 때 애플리케이션에 등록된 서블릿 클래스를 찾아 동적으로 인스턴스를 생성하고 메서드를 호출
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.getWriter().write("Hello, Servlet!");
}
}
2. 스프링 프레임워크 : 의존성 주입, 빈 등록을 위한 리플렉션
@Component
public class UserService {
}
3. ORM (mybatis, Hibernate) : 객체와 데이터베이스 테이블을 자동으로 매핑 :(resultSet.getString("id") → user.setId(value))
4. 테스트 프레임워크 (JUnit) : 테스트를 위해 private 필드에 강제로 값을 넣거나 변경 ,Mock 객체 생성 및 주입
import java.lang.reflect.Field;
class User {
private String name = "userA";
}
public class ReflectionTest {
public static void main(String[] args) throws Exception {
User user = new User();
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "userB");
System.out.println(field.get(user)); // "userB"
}
}
이렇게 간단하게 리플렉션에 대해 알아보았습니다
리플렉션과 관련해서 애노테이션에 대해서도 정리하고 포스팅 하는 시간을 가지도록 하겠습니다
감사합니다.
'Java' 카테고리의 다른 글
[Java] 스레드 풀과 Excutor 프레임워크 (4) | 2024.09.29 |
---|---|
[Java]ReentrantLock (9) | 2024.09.18 |
자바의 고오급 동기화 concurrent.Lock (7) | 2024.09.14 |
Java 프로세스와 스레드 (4) | 2024.08.30 |