본문 바로가기

개발

리플렉션을 쓰기 전에 생각해야 하는 것들

반응형

Spring을 공부하면서 자주 마주치는 개념 중 하나가 바로 리플렉션 입니다.
그동안 "클래스 정보를 다룰 수 있다"는 정도로만 알고 있었지만, 실제로 어떤 기능을 하고 어떻게 활용되는지는 다소 추상적으로 느껴졌습니다. 이번 기회에 리플렉션에 개념을 보다 명확하게 정리하고, Spring 내부에서 어떻게 사용되는지를 이해해보기 위해 이 글을 작성하게 됬습니다.

 


 

 

 

리플렉션 (Reflection)?

자바 프로그램이 실행 중에 클래스의 메타정보(클래스 이름, 필드, 메서드 등)를 조회하고,

심지어 수정하거나 호출까지 할 수 있는 기능을 말합니다.

즉, 컴파일 타임이 아닌 런타임에 어떤 객체의 구조를 알 수 있고, 해당 객체에 동적으로 접근하거나 조작할 수 있습니다.

 

 

 

 

 

리플렉션으로 할 수 있는 일들

리플렉션은 클래스, 필드, 메서드, 생성자 등의 정보를 런타임에 동적으로 조작 할 수 있는 기능을 제공 하고

이를 통해 일반적인 코드로는 접근 할 수 없는 정보나 기능에 접근하거나, 유연한 코드를 작성할 수 있게 해줍니다.

 

리플렉션의 다양한 기능

  • 클래스 정보 조회
  • 필드 정보 접근 및 수정
  • 메서드 호출
  • 생성자 호출 및 객체 생성
  • 어노테이션 정보 조회

이러한 기능들은 테스트 코드 작성, 동적 프록시 생성 등 다양한 곳에서 활용 됩니다.

특히 Spring 프레임워크에서는 빈 등록, 의존성 주입, AOP 구현 등 리플렉션이 기술이 사용됩니다.

 

예제코드

리플렉션 예제를 활용 해볼 ReflectionTargetServic를 만들어 줍니다.

public class ReflectionTargetService {
    public String publicName;
    private int age;
    protected int protectedYear;

    public void publicMethod() {
        System.out.println("Public 메소드를 실행했습니다.");
    }

    private void privateMethod() {
        System.out.println("Private 메소드를 실행했습니다.");
    }

    protected void protectedMethod() {
        System.out.println("Protected 메소드를 실행했습니다.");
    }
}

 

1. 클래스 정보 조회

// 1. 클래스 객체 얻기
Class<?> clazz = ReflectionTargetService.class;

// 2. 클래스 이름 출력
System.out.println("클래스 이름: " + clazz.getName());
// 클래스 이름: study.reflection.ReflectionTargetService

// 3. 필드 목록 출력
System.out.println("\n[필드 목록]");
for (Field field : clazz.getDeclaredFields()) {
    System.out.println("- " + field.getType().getSimpleName() + " " + field.getName());
}
// [필드 목록]
// - String publicName
// - int age
// - int protectedYear

// 4. 메서드 목록 출력
System.out.println("\n[메서드 목록]");
for (Method method : clazz.getDeclaredMethods()) {
    System.out.println("- " + method.getName());
}
// [메서드 목록]
// - privateMethod
// - protectedMethod
// - publicMethod
}

 

 

2. private 필드 및 메서드 접근

// 1. 인스턴스 생성
ReflectionTargetService target = new ReflectionTargetService();

// 2. 클래스 객체 얻기
Class<?> clazz = target.getClass();

// 🔸 private 필드 접근 및 값 설정
Field privateField = clazz.getDeclaredField("age");
privateField.setAccessible(true); // 접근 허용
privateField.set(target, 30); // 값 설정

System.out.println("private 필드 age의 값: " + privateField.get(target));
// private 필드 age의 값: 30

// 🔸 private 메서드 호출
Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true); // 접근 허용
privateMethod.invoke(target); // 메서드 실행
// Private 메소드를 실행했습니다.
  • getDeclaredField("age") : 클래스 내부의 age 필드 객체를 가져옵니다
  • setAccessible(true) : 접근 제어자 무시: private, protected 필드/메서드도 접근 가능하게 설정
  • set(target, value) : 대상 인스턴스의 해당 필드 값을 설정
  • invoke(target) : 대상 인스턴스의 메서드 실행

 

3. 생성자 호출 및 객체 생성

// 클래스 객체 가져오기
Class<?> clazz = Class.forName("study.reflection.ReflectionTargetService");

// 기본 생성자 가져오기
Constructor<?> constructor = clazz.getConstructor(); // public 생성자만 가져옴
Object instance = constructor.newInstance(); // 객체 생성

System.out.println("생성된 객체: " + instance.getClass().getName());
// 생성된 객체: study.reflection.ReflectionTargetService
  • Class.forName("...") : 클래스 이름을 문자열로 받아 해당 클래스의 Class 객체 반환
  • getConstructor() : public 기본 생성자를 반환
  • newInstance() : 해당 생성자를 통해 객체를 생성

 

만약 생성자가 파라미터를 받는다면, 아래처럼 사용할 수 있다.

Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object instance = constructor.newInstance("name", 10);

 

Spring에서 쓰이는 리플렉션

리플렉션의 대표적인 활용 사례 중 하나는 바로 Spring의 의존성 주입 기능입니다.

예를 들어 다음과 같이 @Autowired를 사용한 필드 주입 코드를 자주 볼 수 있는데요.

@autowired
private ReflectionTargetService reflectionTargetService

이 경우 reflectionTargetService는 private으로 선언되어 있지만, Spring은 어떻게 이 필드의 외부 Bean 객체를 "주입" 할까요?

이 글을 처음부터 읽으셨던 분들을 이미 눈치채셨겠지만 바로 리플렉션 입니다.

Spring은 내부적으로 다음과 같은 과정을 통해 의존성을 주입합니다.

  1. 클래스의 필드 정보를 리플렉션으로 분석
  2. @Autowired가 붙은 필드를 찾음
  3. setAccessible(true)를 통해 private 필드에 접근 가능한 상태로 변경
  4. 알맞은 Bean 객체를 해당 필드에 직접 주입

즉, 접근 제한자(private 등)를 우회해서 주입이 가능하도록 리플렉션이 사용되는것 입니다.

 

⚠️ 하지만, 생성자 주입이 더 권장됩니다

최근에는 위와 같은 필드 주입 방식보다 생성자를 통한 주입 방식이 더 많이 사용 되고 있습니다.

@Component
public class MyService {

    private final ReflectionTargetService reflectionTargetService;
    
    public MyService(ReflectionTargetService reflectionTargetService) {
        this.reflectionTargetService = reflectionTargetService;
    }
}

생성자 주입을 더 권장하는 이유

생성자 주입을 권장하는 이유는 테스트가 용이하고, 순환 참조 방지, 명시적, 명확한 의존성 등등 다양한 이유가 있지만 오늘은 리플렉션에 대해 다루는 글이기때문에 주입에 관한 내용은 다음에 따로 정리해보도록 하겠습니다.

 

 

리플렉션의 장단점

 

🌟 장점

  • 동적 객체 조작 가능
    컴파일 시점이 아닌 런타임에 클래스 정보를 조작할 수 있어, 유연한 코드 작성 가능
  • 프레임워크, 라이브러리 제작에 용이
    Spring, Hibernate, Junit 등은 내부적으로 리플렉션을 활용해 의존성 주입, 메소드 실행 등을 처리
  • 코드의 재사용성과 범용성 증가
    다양한 타입을 처리할 수 있는 범용적인 코드 작성 가능(JSON 파서, ORM 매핑 등)

 

⚠️ 단점

  • 성능 저하
    리플렉션은 런타임에 동작하기 때문에 일반 메소드 호출보다 속도가 느림
  • 컴파일 타입 안정성 저하
    코드 오류가 컴파일 시점에 발견되지 않고 런타임에 발생할 수 있어 디버깅과 유지보수가 어려움
  • 접근 제한 무시 가능 -> 보안 위험
    private 필드, 메서드에도 접근 가능에 보안적으로 취약해질 수 있음
  • 복잡성과 가독성 저하
    코드가 난해하고 이해하기 어려워 질 수 있음

 

 

 

📌 정리

리플렉션에 대해 정리하면서 많은 생각이 들었다. 리플렉션은 현관 열쇠를 잃어버렸을 때 사용할 수 있는 비상키 같은 존재라고.

실제로 리플렉션은 강력한 기능을 제공하지만, 그만큼 위험성도 따르는 양날의 검 같은 기능이다.

제대로 된 상황에서 올바르게 사용하면 매우 고마운 도구지만 오용하거나 남용하면 오히려 문제를 일으키는 원인이 될 수 있다.

유연하고 강력한 기능이지만, 그만큼 성능 저하, 보안 문제, 유지보수의 어려움 등의 단점도 분명하다.

될 수 있으면 리플렉션의 사용을 지양하고, 정말 필요한 순간에만 신중하게 사용하는 것이 좋다고 생각한다.

반응형