[Java] - Reflection

chancehee·2022년 12월 20일
0

자바

목록 보기
11/12
post-thumbnail
post-custom-banner

0. 글을 작성하는 이유

개발을 하다보면 익숙하게 사용하는 기술의 동작원리가 궁금해질 때가 있습니다.
Spring은 어떻게 실행시점에 빈을 주입할 수 있는 걸까?
JPA의 Entity는 왜 꼭 기본 생성자를 가져야만 할까?
두 질문을 관통하면 키워드에는 Reflection이라는 것이 있습니다.
Java를 사용하면서 Reflection이라는 단어를 들어본 경험이 한 번 쯤음 있을 것입니다.
그렇지만 평소 잘 사용하지 않는 단어이고 익숙하지 않은 사람들이 대부분일 것이라고 생각합니다.
오늘은 Reflection이 무엇인지 알아보고 도대체 왜 쓸까 그리고 어디에 활용이 가능할지 알아보겠습니다.

1. Reflection

사전적 의미는 '거울에 반사된 상' 🪞↪️
동적으로 클래스 정보에 접근해서 객체 생성, 메서드 실행 등 처리를 가능하게 해주는 API로 java.lang.reflect 패키지를 제공합니다.

[JVM 메모리 영역]

  • .java 소스코드 -> .class 바이트코드
  • 변환된 바이트코드를 클래스 로더가 읽어 들입니다.
  • 클래스 데이터는 메서드 영역에 인스턴스 정보는 힙 영역에 저장됩니다.
  • 즉, 실체=Class, 거울=JVM 메모리 영역을 의미합니다.

[리플렉션 핵심]

  • 클래스 정보 접근
  • 메서드 정보 접근
  • 객체 생성 및 메서드 실행하기

아래에서는 리플렉션 핵심 3가지를 살펴보겠습니다.

핵심1 : 클래스 정보 접근

클래스 정보 접근이란 JVM 코드표 영역에 접근하는 것 입니다.

접근하는 방법으로는 3가지 방법이 존재합니다.

[접근 방법]
1. Class<?> {클래스이름}.class
2. Class<?> Class.forName(String {클래스이름})
3. Class<?> {객체이름}.getClass()

이렇게 3가지 방법을 통해서 상황에 맞게 사용이 가능하겠네요~.~

[예제]

// 가져온 클래스 정보를 통해, 클래스 상세 정보들 조회하기
Class<?> clz = {클래스이름}.class; // 1번 접근 방법 사용 

System.out.println(clz.getName()); // com.XXX.XXX.클래스이름
System.out.println(clz.getSimpleName()); // 클래스이름 

핵심2 : 메서드 정보 접근

[접근 방법]
1. Method[] Class.getDeclaredMethods()
2. Method getDeclaredMethod(String name, Class<?> … param Types)

[예제]

// 가져온 클래스의 메서드를 가져오기
Class<?> clz = 클래스이름.class;

Method[] mArr = clz.getDeclaredMethods();

// 1. 클래스가 가지고 있는 모든 메서드 조회 
for (Method m : mArr) {
	System.out.println("메서드 이름: " + m.getName());
}

// 2. 클래스가 가지고 있는 1개의 메서드 조회
Method m = clz.getDeclaredMethod("메서드 이름", 자료형.class);
System.out.println(m.getName());

핵심 3 : 객체 생성 및 메서드 실행하기

1) 객체 생성

  • 생성자 얻기 : Constructor<?> Class.getDeclaredConstructor(Class<?> … params)

  • 객체 생성 : Object Constructor newInstance(Object … args)

    [예제]

    // 클래스 클래스이름 = new 클래스();  사실 이게 하고 싶음!!...
    
    // 위와 동일한 결과로 생성이 가능하다 !! 
    Class<?> clz = 클래스.class;
    Constructor<?> constructor = clz.getDeclaredConstructor(); // 파라미터가 없기 때문에 기본 생성자를 가져온다.
    클래스 클래스이름 = constructor.newInstance();
    
    // 클래스 클래스이름 = 클래스.class.getDeclaredConstructor().newInstance(); // 인라인 코드
    
    // 근데 이걸 왜 함?? 클래스에 관한 정보를 지정해 줄 수 없는 동작 시점에 사용이 가능하다 ㄷㄷ 
    // 동적으로 동작하게 만들기 위한 원리이다.
    // 리플렉션을 이용하여 객체 생성 

2) 메서드 실행

  • 메서드 실행 : Object Method invoke(Object target, Object … args)

2. Reflection API 활용 예

  • Jackson, Gson 등의 JSON Serialization Library
  • Log4 j2, Logback 등의 Logging Framework
  • Apache Commons BeanUtils 등의 Class Verification API
  • Spring의 @Autowired와 같은 DL, DI 기능 (: ProcessInject(), Inject() Method)
  • Spring Controller의 BeanFactory 에서 사용
  • 내부적으로는 Spring의 ReflectionUtils라는 Abstraction Library를 사용합니다.
  • Eclipse, Intellij 등의 IDE, JUnit, Hamcrest와 같은 Test Framework

3. 쓰는 이유

  • Java는 기본적으로 컴파일러를 사용합니다. (물론 Java는 컴파일러와 인터프리터를 모두 사용하는 JIT 컴파일러)
    즉, 컴파일 시점에 타입을 결정합니다.

  • Reflection API를 통해서, 클래스의 이름만 가지고도 이미 로딩이 완료된 클래스에서 또 다른 클래스를 동적으로 로딩하여 생성자, 멤버 필드 그리고 멤버 메서드 등을 사용할 수 있도록 합니다.

  • 주로 프레임워크와 라이브러리에서 사용하는데, 컴파일 시점엔 객체의 타입을 모르기 때문에 동적으로 해결하기 위해서 사용합니다.

    Spring 교육을 들으면서, 클래스 객체를 만들 때 “기본 생성자를 꼭 만들어줘야한다. 안그러면 쫑난다.” 라는 말을 담당 교수님에게 들었던 적이 있습니다.
    왜 만들어줘야할까?.. 그 당시에는 잘 몰랐지만, Reflection을 공부하면서 알게된 내용은 이러합니다.

    Reflection이 가져올 수 없는 정보 중 하나는 바로 생성자의 인자 정보들 입니다.
    따라서 기본 생성자 없이 파라미터가 있는 생성자가 존재한다면 Reflection이 객체를 생성할 수 없게 됩니다.
    (기본 생성자를 사용하는 이유는?
    기본 생성자로 객체를 생성하고, 필드를 통해 값을 넣어주는 것이 가장 간단한 방법이기 때문입니다.)

4. 단점

  1. 일반 메서드 호출보다 성능이 훨씬 떨어진다.
    • Reflection API는 컴파일 시점이 아니라 런타임 시점에서 클래스를 분석합니다.
    • 따라서 JVM을 최적화할 수 없기 때문에 성능저하가 발생합니다.
  2. 컴파일 시점에서 타입 체크 기능을 사용할 수 없다.
  3. 코드가 지저분하고 장황해진다.
  4. 내부를 노출해서 추상화를 파괴한다.

5. 결론

Reflection은 JVM 영역내에 존재하는 클래스의 정보를 가져와 사용할 수 있기 때문에 무궁무진한 활용이 가능합니다.
그렇지만, 거치게 되는 단계가 그만큼 많아 지는 것이므로 여러가지 단점을 수반합니다.
따라서 꼭 필요한 경우에만 사용하는 것을 권장합니다 😎

출처:
https://www.flaticon.com/kr/free-icon/infinity_5117822
https://www.youtube.com/watch?v=67YdHbPZJn4

post-custom-banner

0개의 댓글