20241231 TIL : Java Reflection

MCS·2024년 12월 31일

TIL

목록 보기
35/45

오늘 학습한 내용

  • Java Reflection
    • 리플렉션이란?
    • 리플렉션 사용 예시
    • 리플렉션은 언제 사용되는가?

Java Reflection

AOP와 프록시를 정리한 내용에서 리플렉션이 언급되었는데, 리플렉션이 무엇인지 정리해 보았다.

리플렉션이란?

리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API이다.
여기서 로드된 클래스란, JVM 클래스 로더에서 클래스 파일에 대한 로딩을 완료한 후, 해당 클래스의 정보를 담은 Class 타입의 객체를 생성해 메모리의 힙 영역에 저장해 둔 것을 의미한다.

구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며, 컴파일된 바이트 코드를 통해 런타임에 동적으로 특정 클래스의 정보를 추출할 수 있다.

리플렉션으로 할 수 있는 것들을 나열해 보면 다음과 같다.

  • 필드 가져오기
  • 메서드 가져오기
  • 상위 클래스 가져오기
  • 인터페이스 가져오기
  • 어노테이션 가져오기
  • 생성자 가져오기
  • 생성자를 통해 객체 생성하기

리플렉션 사용 시 주의점

리플렉션을 사용하는 경우 private으로 선언된 필드나 메서드에도 접근이 가능하다. 하지만 private 데이터에 접근하는 것 자체가 캡슐화를 깨트리는 위험성이 있으므로 주의해야 한다.

또한 리플렉션은 메서드 호출이나 필드 접근 시, JVM은 메타데이터를 통해 객체의 구조를 탐색하고 해당 요소를 찾아야 한다. 런타임에 바인딩이 이루어지게 되므로 추가 리소스를 소모하게 되고, 컴파일 시 에러 검출이 불가능해진다.

리플렉션 시 JVM의 메타데이터에 접근하게 되는데, 이를 탐색하는 작업이 일반적인 메서드 호출보다 더 많은 비용이 발생하며 필드나 메서드가 많을수록 비용이 더 발생할 수 있다.

리플렉션은 캐싱을 사용하지 않으므로, 동일한 필드나 메서드에 대해 반복적으로 리플렉션을 사용하더라도 매번 메타데이터를 재탐색해 이에 대한 비용이 발생한다.

리플렉션 사용 예시 코드

다음 코드는 GPT를 통해 위에서 나열한 리플렉션으로 할 수 있는 것들을 수행하는 코드이다.

import java.lang.annotation.*;
import java.lang.reflect.*;

// 예제 클래스 정의
@CustomAnnotation
class ExampleClass extends ParentClass implements ExampleInterface {
    private String privateField;
    public int publicField;

    public ExampleClass() {}
    public ExampleClass(String privateField) {
        this.privateField = privateField;
    }

    public void exampleMethod() {
        System.out.println("Example method executed");
    }

    private void privateMethod() {
        System.out.println("Private method executed");
    }
}

class ParentClass {}
interface ExampleInterface {}

@Retention(RetentionPolicy.RUNTIME)
@interface CustomAnnotation {}

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 대상 클래스
        Class<?> clazz = ExampleClass.class;

        // 1. 필드 가져오기
        System.out.println("Fields:");
        for (Field field : clazz.getDeclaredFields()) {
            System.out.println(" - " + field.getName());
        }

        // 2. 메서드 가져오기
        System.out.println("\nMethods:");
        for (Method method : clazz.getDeclaredMethods()) {
            System.out.println(" - " + method.getName());
        }

        // 3. 상위 클래스 가져오기
        System.out.println("\nSuperclass:");
        System.out.println(" - " + clazz.getSuperclass().getName());

        // 4. 인터페이스 가져오기
        System.out.println("\nInterfaces:");
        for (Class<?> iface : clazz.getInterfaces()) {
            System.out.println(" - " + iface.getName());
        }

        // 5. 어노테이션 가져오기
        System.out.println("\nAnnotations:");
        for (Annotation annotation : clazz.getAnnotations()) {
            System.out.println(" - " + annotation.annotationType().getName());
        }

        // 6. 생성자 가져오기
        System.out.println("\nConstructors:");
        for (Constructor<?> constructor : clazz.getConstructors()) {
            System.out.println(" - " + constructor);
        }

        // 7. 생성자를 통해 객체 생성하기
        System.out.println("\nCreating object using constructor:");
        Constructor<?> constructor = clazz.getConstructor(String.class);
        Object instance = constructor.newInstance("Reflection Example");
        System.out.println("Instance created: " + instance);

        // Private 메서드 접근
        Method privateMethod = clazz.getDeclaredMethod("privateMethod");
        privateMethod.setAccessible(true); // private 접근 허용
        privateMethod.invoke(instance); // 메서드 호출
    }
}

리플렉션은 언제 사용되는가?

스프링 프레임워크는 리플렉션을 광범위하게 사용하여 런타임에 다양한 기능을 제공한다. 주요 활용 사례는 다음과 같다.

  1. 의존성 주입(DI)과 제어의 역전(IoC)
    스프링은 리플렉션을 사용하여 빈(bean)을 생성하고, @Autowired 또는 @Qualifier가 붙은 필드나 생성자에 의존성을 주입한다.
    필드나 메서드의 접근 제한자(private)를 무시하고 값을 주입하기 위해 리플렉션으로 setAccessible(true)를 설정한다.
  2. AOP(Aspect-Oriented Programming)
    리플렉션을 사용하여 메서드, 클래스, 어노테이션 등을 동적으로 탐지하고, 프록시를 생성하여 부가 기능(로깅, 트랜잭션 등)을 추가한다.
    예: @Transactional이 붙은 메서드에 트랜잭션 관리 로직을 추가
  3. 스프링 MVC 요청 처리
    컨트롤러 메서드를 호출할 때, 메서드 파라미터와 반환 타입을 리플렉션으로 분석하여 적절히 매핑한다.
    예: @RequestMapping이 붙은 메서드의 URL 패턴을 분석하고, 요청을 해당 메서드로 라우팅
  4. 빈 초기화 및 라이프사이클 관리
    @PostConstruct, @PreDestroy 등 라이프사이클 어노테이션을 리플렉션으로 탐지하고, 관련 메서드를 호출한다.
    빈 정의 정보(XML, Java Config 등)를 기반으로 동적으로 객체를 생성한다.
  5. 스프링 데이터 JPA
    엔티티 클래스의 필드와 어노테이션 정보를 읽어 데이터베이스 테이블과 매핑한다.
    예: @Entity, @Id, @Column 어노테이션을 분석하여 SQL 쿼리를 동적으로 생성
  6. 어노테이션 처리
    커스텀 어노테이션과 메타 어노테이션을 분석하여 특정 동작을 수행한다.
    예: @Component, @Service, @Repository 등 스캔을 통해 빈으로 등록
  7. 프로퍼티 설정 및 접근
    Environment나 @Value를 통해 프로퍼티 값을 주입할 때, 리플렉션을 사용해 필드에 접근하고 값을 설정한다.
  8. 이벤트 리스너 관리
    @EventListener 어노테이션이 붙은 메서드를 분석하여 이벤트를 동적으로 연결하고 호출한다.

프록시에 대한 내용을 정리할 때 어떻게 리플렉션을 활용하고 프록시 객체를 사용하는지는 정리했었다. 굉장히 광범위하게 사용되고 있는 것을 알 수 있다.

profile
백엔드를 잘 하고 싶은 사람

0개의 댓글