[Java] Reflection 알아보기

joowonseo·2025년 3월 6일
0
post-thumbnail

Java Reflection실행 중인 Java 프로그램이 자신의 구조를 검사하고 수정할 수 있게 해주는 API입니다. 코드가 실행되는 동안 클래스, 인터페이스, 필드, 메서드의 내부 정보에 접근하여 프로그램을 더 동적으로 만들 수 있는 기능입니다.

🪞 Java Reflection

Reflection은 자바 프로그래밍 언어에서 제공하는 API로, 실행 중인 프로그램의 클래스, 인터페이스, 필드, 메서드에 대한 정보를 조사하고 동적으로 조작할 수 있게 해줍니다. 이렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, reflection이라 합니다.
이 기능은 java.lang.reflect 패키지에 포함되어 있습니다.

Java Reflection을 통해 할 수 있는 주요 작업들은 다음과 같습니다.

  • 클래스의 모든 멤버(필드, 메서드, 생성자 등) 검색
  • 접근 제한자에 관계없이 객체의 필드 값 가져오기 및 수정하기
  • 메서드를 동적으로 호출하기
  • 런타임에 객체 생성하기
  • 애너테이션 정보 조회하기

🧐 어디서 클래스의 정보를 가져오는가?

바로 JVM의 메모리 영역입니다. 애플리케이션을 실행하면 작성한 자바 코드는 컴파일러에 의해 .class 형태의 바이트 코드로 변환되고, 이 정보들은 클래스 로더를 통해 JVM 메모리 영역에 저장됩니다. 그리고 클래스 정보를 통해 객체가 생성된다면 이는 JVM 힙 영역에 저장됩니다.

즉, Reflection은 JVM 메모리 영역에 로드된 클래스의 메타데이터에 접근하여 해당 정보를 가져오고 조작하는 것입니다. 이것이 Reflection이 런타임에 동작할 수 있는 이유이자, 컴파일 타임 검사를 우회할 수 있는 이유이기도 합니다.

Reflection 사용 예시

클래스 정보 가져오기

// 클래스 정보 가져오기
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("클래스 이름: " + clazz.getName());
System.out.println("단순 클래스 이름: " + clazz.getSimpleName());
System.out.println("패키지 이름: " + clazz.getPackageName());

클래스의 멤버 검사하기

// 모든 메서드 확인하기
Method[] methods = clazz.getDeclaredMethods();
System.out.println("클래스의 메서드 목록:");
for (Method method : methods) {
    System.out.println("\t" + method.getName() + ": " + method.getReturnType());
}

// 모든 필드 확인하기
Field[] fields = clazz.getDeclaredFields();
System.out.println("클래스의 필드 목록:");
for (Field field : fields) {
    System.out.println("\t" + field.getName() + ": " + field.getType());
}

객체 생성 및 메서드 호출

// 리플렉션으로 객체 생성
Class<?> stringClass = Class.forName("java.lang.String");
Constructor<?> constructor = stringClass.getConstructor(String.class);
Object stringObject = constructor.newInstance("Hello Reflection");

// 메서드 호출
Method toUpperCaseMethod = stringClass.getMethod("toUpperCase");
Object result = toUpperCaseMethod.invoke(stringObject);
System.out.println(result);  // "HELLO REFLECTION" 출력

private 필드 접근하기

단순히 조회 뿐 아니라 멤버필드의 값도 수정할 수 있습니다.
Setter 메서드가 없어도, 접근제어자가 private라도 이를 무시하고 값을 바꿔버릴 수 있는 것 입니다.

public class Person {
    private String name;
    
    public Person(String name) {
        this.name = name;
    }
}

// 메인 코드
Person person = new Person("John");
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true);  // private 접근 제한자 우회
String name = (String) nameField.get(person);
System.out.println("이름: " + name);  // "이름: John" 출력

// 필드 값 변경
nameField.set(person, "Jane");
System.out.println("변경된 이름: " + nameField.get(person));  // "변경된 이름: Jane" 출력

Reflection을 사용하는 이유

프레임워크 개발

Spring, Hibernate, JUnit과 같은 프레임워크들은 Reflection을 광범위하게 사용합니다. 예를 들어, Spring은 애너테이션이 달린 클래스를 스캔하고 빈으로 등록하기 위해 Reflection을 사용합니다.

테스트 도구 구현

JUnit과 같은 테스트 프레임워크는 @Test 애너테이션이 달린 메서드를 찾고 실행하기 위해 Reflection을 사용합니다.

플러그인 시스템 구현

런타임에 플러그인을 로드하고 통합해야 하는 시스템에서는 Reflection이 필수적입니다. 컴파일 시점에 존재하지 않았던 클래스를 동적으로 로드하고 사용할 수 있습니다.

직렬화 및 역직렬화

객체를 파일이나 네트워크로 전송하기 위해 JSON, XML 등으로 변환할 때, Reflection은 객체의 모든 필드를 쉽게 접근할 수 있게 해줍니다.

ORM(Object-Relational Mapping)

Hibernate와 같은 ORM 도구는 Java 객체와 데이터베이스 테이블 간의 매핑을 위해 Reflection을 사용합니다.


Reflection의 장점

동적 코드 실행

컴파일 시 알려지지 않은 클래스, 메서드, 필드에 접근할 수 있어 매우 유연한 코드를 작성할 수 있습니다.

프레임워크 및 라이브러리 구현 가능

Reflection은 프레임워크 개발자들이 유연하면서도 강력한 도구를 만들 수 있게 해줍니다. Spring, Hibernate, JUnit 등이 대표적입니다.

테스트 용이성

프라이빗 필드와 메서드에 접근할 수 있어 테스트가 더 쉬워집니다.

유지보수 개선

경우에 따라, Reflection을 사용하면 더 적은 코드로 더 많은 기능을 구현할 수 있어 유지보수가 쉬워질 수 있습니다.

Reflection의 단점

성능 저하

Reflection 호출은 일반 메서드나 필드 접근보다 훨씬 느립니다. JVM 최적화를 우회하며, 추가적인 오버헤드가 발생합니다.

// 일반적인 방법
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
    String str = "hello";
    str = str.toUpperCase();
}
System.out.println("General: " + (System.nanoTime() - start) + "ns");

// Reflection 방법
start = System.nanoTime();
Method toUpperCase = String.class.getMethod("toUpperCase");
for (int i = 0; i < 1000000; i++) {
    String str = "hello";
    str = (String) toUpperCase.invoke(str);
}
System.out.println("Reflection: " + (System.nanoTime() - start) + "ns");

위 코드를 실행하면 Reflection이 일반 방법보다 3배 이상 느린 것을 확인할 수 있습니다.

컴파일 타임 검사 우회

Reflection은 컴파일 타임 타입 검사를 우회하므로, 런타임 에러가 발생할 가능성이 높아집니다.

// 컴파일 시 발견 불가능한 오류
try {
    Class<?> clazz = Class.forName("com.example.NonExistentClass");
    Method method = clazz.getMethod("nonExistentMethod");
} catch (Exception e) {
    System.out.println("런타임 오류 발생: " + e.getMessage());
}

보안 위험

Reflection은 접근 제한자(private, protected)를 우회할 수 있어 캡슐화를 깨트릴 수 있습니다. 이는 의도하지 않은 부작용을 일으킬 수 있으며, 보안 취약점이 될 수 있습니다.

복잡성 증가

Reflection을 사용한 코드는 일반 코드보다 더 복잡하고 이해하기 어려울 수 있습니다. 또한, 디버깅도 어려워집니다.

API 변경에 취약함

Reflection으로 접근하는 클래스의 내부 구조가 변경되면, 런타임 오류가 발생할 수 있습니다.



🧐 언제 Reflection을 사용해야 할까?

Reflection은 강력한 도구이지만 모든 상황에 적합하지는 않습니다.
다음과 같은 상황에서 주로 사용하는 것이 좋습니다.

  1. 프레임워크나 라이브러리를 개발할 때
  2. 다양한 유형의 객체를 일관된 방식으로 처리해야 할 때
  3. 플러그인 아키텍처를 구현할 때
  4. 디버깅 도구나 IDEs를 개발할 때

반면, 다음과 같은 상황에서는 Reflection 사용을 피하는 것이 좋습니다.

  1. 성능이 중요한 애플리케이션
  2. 간단한 문제를 해결할 때 (과도한 엔지니어링 피하기)
  3. 보안이 중요한 영역

📝 정리하기

1. 정의

Java Reflection은 런타임에 클래스, 메서드, 필드 등의 정보를 동적으로 조회하고 조작할 수 있도록 해주는 기능입니다. 이를 활용하면 객체의 타입을 미리 알지 않고도 메서드를 호출하거나 필드 값을 수정할 수 있습니다.

2. ✅ 장점

  • 유연성 증가

    • 컴파일 시점이 아니라 실행 시점에 클래스 및 메서드를 동적으로 사용 가능
    • 플러그인 시스템, 의존성 주입, ORM, 테스트 프레임워크에서 활용
  • 프레임워크 및 라이브러리 개발에 필수적

    • Spring, Hibernate 같은 프레임워크는 Reflection을 활용하여 동적으로 빈을 생성하거나 필드를 주입
  • 컴파일 의존성 제거

    • 런타임에 특정 클래스를 로드할 수 있어, 동적으로 확장 가능한 애플리케이션 개발 가능

3. ⚠️ 단점

  • 성능 저하

    • Reflection을 사용하면 JVM 최적화가 어렵고, 직접 메서드를 호출하는 것보다 속도가 느림
  • 타입 안전성 부족

    • 일반적인 Java 코드와 달리 컴파일 타임에 오류를 잡을 수 없고, 실행 시 NoSuchMethodException 등의 예외가 발생할 가능성 존재
  • 보안 문제

    • Private 필드 및 메서드에도 접근 가능하여, 보안 취약점을 초래할 위험이 있음
  • 디버깅 어려움

    • Reflection을 사용하면 코드 흐름을 추적하기 어려워 유지보수가 복잡해질 수 있음

4. 사용 예시

  • 의존성 주입(DI)과 빈 생성: Spring이 필요한 의존성을 주입하거나, 어노테이션을 기반으로 빈을 자동으로 생성할때 사용
  • 어노테이션 처리: Spring은 메서드나 클래스에 정의된 어노테이션을 런타임에 분석하여 특정 동작을 수행하는데, 이 처리를 reflection이 수행
  • 동적으로 클래스 사용: 코드 작성 시점에 어떤 클래스를 사용할지 모를 때, 런타임에 클래스를 로드하여 동적으로 객체를 생성하고 활용
  • 테스트 코드 작성: private 메서드나 변수에 접근하여 테스트할 때 리플렉션을 사용하여 접근 제어를 우회
  • 자동 매핑 기능 구현: IDE에서 사용자가 입력한 일부 문자열을 기반으로 관련된 클래스나 메서드를 자동으로 제시하는 기능을 구현할 때 리플렉션을 활용하여 메타데이터를 조회

5. 대안 기술

애너테이션 프로세싱 (Annotation Processing)

  • Lombok, MapStruct 등이 애너테이션 기반 코드 생성으로 Reflection을 대체
    동적 프록시 (Dynamic Proxy)
  • java.lang.reflect.Proxy를 활용한 인터페이스 기반 동적 프록시 생성
    코드 생성 라이브러리 (Bytecode Manipulation)
  • ASM, Javassist 등을 이용해 바이트코드를 직접 조작

📗 참고

https://tlatmsrud.tistory.com/112
파랑, 아키의 리플렉션 - https://www.youtube.com/watch?v=67YdHbPZJn4

profile
백엔드 개발자 ˚₊✩‧₊ ໒꒱

0개의 댓글