본문 바로가기

Java

리플렉션에 대하여

리플렉션이란 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API를 말한다. 

왜 리플렉션을 사용하는가?

리플렉션은 애플리케이션 개발보다는 프레임워크, 라이브러리에서 많이 사용된다.

프레임워크, 라이브러리는 사용하는 사람이 어떤 클래스를 만들지 모른다. 이럴 때 동적으로 해결해주기 위해 리플렉션을 사용한다.

대표적인 사용 예로는 스프링의 DI, Proxy, ModelMapper 등이 있다.

 

구체적인 클래스를 모르는데, 어떻게 접근할 수 있는가?

자바의 클래스 파일들은 바이트코드로 컴파일되어 static과 함께 method 영역에 저장되므로 클래스 이름만 알면 클래스의정보를 찾을 수 있다.

 

예시

@Controller
@RequestMapping("/articles")
public class ArticleController {    

    @Autowired    
    private ArticleService articleService;       
       ....

    @PostMapping
    public String write(UserSession userSession, ArticleDto.Request articleDto){
       ...
    }

    @GetMapping("/{id}")
    public String show(@PathVariable int id, Model model) {
       ...
    }
}

스프링을 사용할 때 @Controller를 넣어주면 인스턴스를 생성하지 않아도 알아서 생성하여 빈으로 관리해준다.

여기서 의문이 드는 점은

  • 스프링은 ArticleController의 존재를 어떻게 알고 만들어주는가
  • ArticleService라는 필드를 어떻게 주입해주는가
  • 모든 메서드의 파라미터 개수, 타입이 다른데 어떻게 알고 해당 값을 바인딩해주나

Article을 작성한 개발자는 클래스의 정보를 알지만 스프링은 모른다.

이러한 프레임워크 상에 문제를 해결하기 위해 리플렉션을 사용하는데, DI와 관련된 클래스들을 스캔하고 해당하는 클래스의 인스턴스 생성 및 필드 DI를 해준다.

클래스와 메서드, 멤버 변수 찾기

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
  public static void main(String[] args) throws ClassNotFoundException {
    //클래스를 찾는 3가지 방법
    Class<?> c1 = Class.forName("java.lang.String");
    Class<?> c2 = String.class;
    Class<?> c3 = Integer.TYPE;

    for(Method method: c1.getDeclaredMethods()){
      System.out.println(method);
    }
    System.out.println("===============END================");
    for(Field field: c2.getDeclaredFields()){
      System.out.println(field.getType() + ", " + field.getName());
    }
    System.out.println("===============END================");

  }
}

출력 결과

byte[] java.lang.String.value()
public boolean java.lang.String.equals(java.lang.Object)
...
===============END================
class [B, value
byte, coder
int, hash
long, serialVersionUID
boolean, COMPACT_STRINGS
class [Ljava.io.ObjectStreamField;, serialPersistentFields
interface java.util.Comparator, CASE_INSENSITIVE_ORDER
byte, LATIN1
byte, UTF16
===============END================

생성자 찾기

생성자도 메서드에 포함되기 때문에 getConstructors를 통한 리플렉션으로 가져올 수 있다. 흥미로운 사실을 발견했는데, 싱글톤 패턴을 파훼할 수 있는 점이다.

class Article{
  private String title;
  private String content;
  private static Article singleton = null;
  private Article(String title, String content){
    this.title = title;
    this.content = content;
  }
  static Article getInstance(){
    if(singleton == null){
      singleton = new Article("A1", "A1");
    }
    return singleton;
  }
}
Class<?> c1 = Article.class;
Article a1 = Article.getInstance();
Article a2 = Article.getInstance();
System.out.println(System.identityHashCode(a1));
System.out.println(System.identityHashCode(a2));
for(Constructor c : c1.getDeclaredConstructors()){
  c.setAccessible(true); //private 생성자의 접근권한을 강제로 true로 바꿈
  System.out.println(System.identityHashCode(c.newInstance("A2", "A2")));
}
/*
1239731077
1239731077
557041912
*/

private 생성자도 끌고와 접근권한을 강제로 바꿔서 새로운 객체를 생성할 수 있다.

이걸 방지하기 위해 Enum을 사용하는데, Enum은 생성한 객체가 모두 공유되기 때문에 리플렉션으로도 깰 수 없다.

 

멤버 변수 변경

위에서 setAccessible로 접근 권한을 강제로 바꿀 수 있음을 알았다. 이를 이용하여 멤버 변수를 변경할 수도 있다.

Article a = Article.getArticle();
Class<?> c1 = Article.class
field field = c1.getDeclaredField("title");
field.setAccessible(true);
field.set(a, "new Title");
System.out.println(a.getTitle());
/*
new Title
*/

리플렉션의 이면

런타임에 동적으로 타입을 분석하고 정보를 가져오므로 JVM을 최적화할 수 없어 성능 오버헤드가 있다.

private field들을 수정할 수 있기 때문에 내부를 노출하여 정보 은닉이 실패한다.

따라서 애플리케이션 개발보단 프레임워크나 라이브러리 제작에 많이 사용된다. 스프링 프레임워크에선 BeanFactory, JPA 엔티티의 값 초기화에 사용된다.

 

참고: JPA 엔티티에 기본 생성자가 필요한 이유는 생성자의 파라미터 정보까진 리플렉션으로 가져올 수 없기 때문에 기본 생성자로 생성하고, 리플렉션으로 값을 초기화하기 때문이다.

'Java' 카테고리의 다른 글

모니터와 락에 대하여  (0) 2022.03.10
Enum에 대하여  (0) 2022.02.26
제네릭에 대하여  (0) 2022.02.26
일급 컬렉션에 대하여  (0) 2022.02.16
Optional에 대하여  (0) 2022.02.10