Post

Java Reflection 정리

Java Reflection

✏️ 개요

  • Java 1.1버전에서 나온 Reflection은 런타임때 클래스나 객체를 생성, 호출하여 동적으로 객체를 다루는 기술을 말한다.

✏️ 나오게 된 배경

Java 프레임워크나 라이브러리 개발시 클래스의 메타데이터를 조작하는데 기능을 제공하기 위해 고안되었다.

즉, 프레임워크에서 제공하는 API를 이용하여 클래스를 호출하고 생성하는 방식이 아닌, 클래스의 이름을 이용하여 동적으로 클래스를 생성하고 호출할 수 있도록 하여 프레임워크의 사용성을 높이게 한다.

✏️ 동적으로 생성시의 이점

Reflection을 이용해 객체를 동적으로 다루는 것은 어떤 이점이 있나

  • 유연성: 코드 내에서 클래스나 객체를 생성하거나 호출하는 방법을 동적으로 바꿔 유연성을 갖게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyClass {

    public void testFunction(){
        System.out.println("hello function");
    }
}

@Test
void Reflection_유연성테스트() throws Exception {
    //경로명을 적어줘야한다.
    String className = "MyClass";
    Class<?> clazz = Class.forName(className);
    Object obj = clazz.getDeclaredConstructor().newInstance();
    Method method = clazz.getMethod("testFunction");
    method.invoke(obj);
}

대표적으로 Spring FrameWork에서 의존성 주입으로 자주 쓰이는 @Autowired를 처리하는 부분은 Reflection을 사용하고 있다.

이는 AutowiredAnnotationBeanPostProcessor라는 클래스로 다음과 같이 구현되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
protected boolean checkAutowiredAnnotation(AnnotatedElement ae) {
    Autowired autowired = ae.getAnnotation(Autowired.class);
    if (autowired != null) {
        if (Modifier.isStatic(ae.getModifiers())) {
            throw new IllegalStateException("Autowired annotation is not supported on static fields");
        }
        return true;
    }
    else {
        return false;
    }
}

@Autowired를 탐색할때 java.lang.reflect.Field, java.lang.reflect.Constructor 클래스를 이용해서 클래스를 사용하고, 해당 필드나 생성자에 값을 주입할 때도 Reflection을 사용한다.

간단하게 인스턴스를 생성해서 해당 인스턴스 필드값을 바꾸는 예제를 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyClass {

    private String name;

    public MyClass(String name){
        this.name = name;
    }

    public MyClass() {
    }


    public void testFunction(){
        System.out.println("hello function");
    }

    public String getName() {
        return name;
    }
}

  • private 이라는 필드에 접근하여 해당 값을 바꿔줄 것이다.
1
2
3
4
5
6
7
8
9
    @Test
    void Reflection_Field_search() throws Exception{
        MyClass clazz = new MyClass();
        // clazz.getField("name"); -> public만 접근가능
        Field name = clazz.getClass().getDeclaredField("name");
        name.setAccessible(true);
        name.set(clazz,"kang min seok");
        System.out.println(clazz.getName());
    }
  • getField()public 필드에만 접근이 가능하다.
  • 때문에 Field.setAccessible(true)를 통해 접근제어자를 풀어줘야한다.

image

성공적으로 값이 바뀌였다.

이를 활용해서 @Autowired기능을 구현해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass {
    private String name;

    @Autowired
    private int age;

    @Autowired
    private OtherClass otherClass;
}

public class OtherClass {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void Reflection_AutowiredTest() throws Exception{
    MyClass bean = new MyClass();

    Field[] fields = bean.getClass().getDeclaredFields();
    for(Field field: fields){
        System.out.println(field.getType());
        if(field.isAnnotationPresent(Autowired.class)){
            Object dependency = applicationContext.getBean(field.getType());
            field.setAccessible(true);
            field.set(bean,dependency);
        }
    }
    //return bean;
}

간단하게 짰지만,

이런식으로 @Autowired가 붙은 모든 필드를 탐색하고 의존성 주입을 넣고 해당 빈을 반환해줄 수 있다.

✏️ 다른 예시

다른 예시로, Java Reflection을 통해 ORM을 구현할 수 있다.

  • ORM은 데이터베이스 테이블과 객체를 매핑하는 기술로, 객체의 필드와 데이터베이스 컬럼을 자동으로 매핑할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Entity
@Builder
public class User {
    @Id
    private int id;
    private String name;
    private int age;
    //getter, setter 생략
}



public class SimpleORM {
    public static void insert(Connection conn, Object object) throws SQLException, IllegalAccessException {
        Class<?> clazz = object.getClass();
        String tableName = clazz.getSimpleName();
        StringBuilder columns = new StringBuilder();
        StringBuilder values = new StringBuilder();

        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            columns.append(field.getName()).append(", ");
            values.append("'").append(field.get(object)).append("', ");
        }

        String sql = "INSERT INTO " + tableName + "(" + columns.substring(0, columns.length() - 2) + ") VALUES(" + values.substring(0, values.length() - 2) + ")";
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.execute();
    }
}

이 코드는 만약 SimpleORM.insert(user)라는 코드를 실행하였을때, user에 있는 필드값들을 쿼리에 순차적으로 넣어서 테이블에 저장이 되는 코드이다.

✏️ 보안적 이슈?

보면 private에도 접근이 가능하고, 다른사람이 만든 클래스에 무작위로 접근해서 값을 바꿀 수도 있을 것 같다.

보안적 이슈가 생길 것 같아서 이를 해결할 수 있는 방법들을 찾아보았다.

  • Security Manager: Java에서는 보안 관리자를 사용하여 클래스나 메소드에 대한 접근 권한을 제어할 수 있다.
  • Input Validation: 특정 클래스에 대한 Reflection을 허용하는 경우 입력값을 적절하게 검증한다. 잘못된 입력값으로 인해 발생할 수 있는 취약점을 방지할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Security Manager
import java.lang.reflect.ReflectPermission;

public class MySecurityManager extends SecurityManager {
    @Override
    public void checkPermission(Permission perm) {
        if (perm instanceof ReflectPermission) {
            if (perm.getName().startsWith("suppressAccessChecks")) {
                if (!perm.getName().startsWith("suppressAccessChecks-com.mypackage")) {
                    super.checkPermission(perm);
                }
            } else {
                super.checkPermission(perm);
            }
        }
    }
}

//main Function
MySecurityManager securityManager = new MySecurityManager();
System.setSecurityManager(securityManager);

이 코드는 com.mypackage패키지에 포함된 클래스만 Reflection을 허용하고, 그 외는 금지시키는 코드이다.

인자로 들어오는 perm의 속성이 com.mypackage로 시작하는지 확인하고 맞다면 권한 체크를 하지 않고 넘어가고, 아니면 SecurityManage에 정의된 메서드를 호출하여 권한 체크를 한다.

1
2
3
4
5
6
7
8
9
//Input Validation
String className = "com.example.MyClass";
if(!className.startsWith("com.example")) {
    throw new IllegalArgumentException("Invalid class name");
}
Class<?> clazz = Class.forName(className);
Object obj = clazz.newInstance();
Method method = clazz.getMethod("performFunction");
method.invoke(obj);

이렇게 입력값을 검증하여 체크하는 방식도 있다. 이 방식을 사용하여 메서드 인자도 검증하여 SQL Injection, XSS등의 취약점을 방지하는데 도움이 될 수 있다니 알아두자.

🙌결론

Java Reflection은 런타임때 클래스, 인터페이스, 필드, 메서드 등을 동적으로 조작하여 코드의 유연함과 코드를 분리하여 유지보수성과 재사용성이 높아지게 하는 기능이다.

This post is licensed under CC BY 4.0 by the author.