리플렉션(Reflection)이란 무엇이며, 언제 사용하나요?

김상욱·2024년 11월 18일

리플렉션(Reflection)이란 무엇이며, 언제 사용하나요?

리플렉션(Reflection)은 프로그래밍 중 실행 시간(runtime)에 클래스, 메서드, 필드 등 객체의 정보를 동적으로 확인하거나 조작할 수 있는 기능. 이를 통해 컴파일 시점에 알 수 없는 객체의 구조를 탐색하거나, 특정 동작을 수행할 수 있음.

Java에서 리플렉션은 java.lang.reflect 패키지에서 제공하는 API를 통해 사용되며, 클래스의 메타데이터(Class Metadata)에 접근하여 프로그램의 유연성을 높일 수 있음.

  • 클래스 이름, 부모 클래스, 인터페이스 등 메타정보 확인 가능. Class 객체를 통해 접근 (Class.forName("클래스명"))

  • 클래스 내 선언된 필드 및 메서드의 이름, 타입, 접근제어자 등의 정보를 확인 가능. getDeclaredFields(), getMethods() 등

  • 컴파일 시점에 클래스 이름을 모르는 경우에도 동적으로 객체를 생성할 수 있음. : Class.newInstance()

  • 특정 메서드를 실행 시간에 호출 가능 : Method.invoke(Object obj, Object... args)

  • 객체의 필드 값을 동적으로 읽거나 수정할 수 있음. Field.set(Object obj, Object value)

  • Spring, Hibernate와 같은 프레임워크는 리플렉션을 활용해 의존성 주입(DI), 애노테이션 처리, 동적 프록시 생성 등을 수행. ex) Spring에서 @Autowired 필드 주입 처리.
    : 런타임에 의존성을 동적으로 주입하는 메커니즘

  • 컴파일 시점에 알 수 없는 클래스나 메서드와 상호작용이 필요할 때. ex) 플러그인 시스템에서 사용자가 추가한 기능을 동적으로 로드.

  • 접근 제한된 필드나 메서드에 접근하여 테스트가 필요할 때. ex) setAccessible(true)를 통해 private 필드 테스트.

  • 데이터 직렬화/역직렬화, JSON 변환 라이브러리(Jackson, Gson 등)에서 클래스 구조에 따라 동적 처리
    : 직렬화와 역직렬화 시 private 필드에도 접근해야 하기 때문.

  • 객체 상태를 분석하거나 메타정보를 로그로 출력할 때.

장점

  • 컴파일 시점에 알 수 없는 클래스나 메서드와 동적으로 상호작용 가능
  • 런타임에 객체를 동적으로 조작할 수 있어 플러그인 개발, 사용자 정의 동작에 유용
    단점
  • 런타임에 많은 작업을 수행하므로 일반 메서드 호출보다 느림
  • private 필드나 메서드에 접근할 수 있어 보안에 취약할 수 있음.
  • 코드 가독성이 떨어지고, 디버깅이 어려울 수 있음.
import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 클래스 로드
        Class<?> clazz = Class.forName("java.lang.String");

        // 메서드 조회
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("Method: " + method.getName());
        }

        // 동적 메서드 호출
        String str = "Hello, World!";
        Method substringMethod = clazz.getMethod("substring", int.class, int.class);
        String result = (String) substringMethod.invoke(str, 0, 5);
        System.out.println("Result: " + result); // 출력: Hello
    }
}

리플렉션은 일반적인 개발에서는 가급적 지양하며, 아래의 경우에만 보통 사용.

  • 애플리케이션이 런타임에 동적으로 동작해야 할 때.
  • 필드, 메서드 등의 정보를 조작해야 할 때.
  • 프레임워크나 라이브러리에서 내부적으로 사용
    -> 남용 시 성능 저하와 복잡성 초래.

신입 개발자의 입장에서 리플렉션 실습 이유

리플렉션은 일반적인 업무에서는 자주 사용되지 않지만, 프레임워크 동작 원리를 이해하고 기본적인 활용 능력을 갖추는 것이 중요합니다. 특히, Java와 Spring 기반 백엔드 개발자에게는 다음과 같은 이유로 실습할 가치가 있습니다:

  1. Spring 프레임워크 이해

    • 리플렉션을 통해 애노테이션 기반 설정, 의존성 주입, AOP 등의 핵심 기능이 어떻게 동작하는지 파악 가능.
    • 실습 예: 애노테이션으로 메서드를 실행하거나, 의존성 주입을 직접 구현해보기.
  2. 테스트 코드 강화

    • 테스트 시 접근 제한(private/protected) 필드나 메서드를 다뤄야 하는 상황에서 활용 가능.
    • 실습 예: setAccessible(true)를 이용한 private 필드 값 변경 및 검증.
  3. 역직렬화 이해

    • 객체 복원 과정에서의 동적 생성과 필드 매핑 과정 학습.
  4. 동적 시스템 개발 능력

    • 사용자 정의 플러그인 로드, 메서드 동적 실행 같은 기능 개발 가능.

실습 예제

1. 클래스 정보 탐색

import java.lang.reflect.Method;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 클래스 정보 탐색
        Class<?> clazz = Class.forName("java.lang.String");

        // 메서드 정보 출력
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("Method: " + method.getName());
        }
    }
}

2. 동적 객체 생성 및 메서드 호출

import java.lang.reflect.Method;

class Sample {
    public void sayHello() {
        System.out.println("Hello, Reflection!");
    }
}

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 클래스 로드
        Class<?> clazz = Class.forName("Sample");

        // 객체 생성
        Object obj = clazz.getDeclaredConstructor().newInstance();

        // 메서드 호출
        Method method = clazz.getMethod("sayHello");
        method.invoke(obj);
    }
}

3. 애노테이션 활용

import java.lang.reflect.Method;

// 사용자 정의 애노테이션
@interface MyAnnotation {
    String value();
}

class MyClass {
    @MyAnnotation("Hello Annotation")
    public void myMethod() {}
}

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 클래스 메타데이터 탐색
        Class<?> clazz = MyClass.class;

        // 메서드 애노테이션 정보 출력
        Method method = clazz.getMethod("myMethod");
        if (method.isAnnotationPresent(MyAnnotation.class)) {
            MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
            System.out.println("Annotation Value: " + annotation.value());
        }
    }
}

4. 간단한 의존성 주입 구현

import java.lang.reflect.Field;

class Service {
    public void serve() {
        System.out.println("Service is serving!");
    }
}

class Controller {
    @Inject
    private Service service;

    public void execute() {
        service.serve();
    }
}

// 의존성 주입 애노테이션
@interface Inject {}

public class DIExample {
    public static void main(String[] args) throws Exception {
        Controller controller = new Controller();

        // 의존성 주입
        for (Field field : controller.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                field.setAccessible(true);
                field.set(controller, new Service());
            }
        }

        // 메서드 실행
        controller.execute(); // 출력: Service is serving!
    }
}

실습 후 확장 학습

  1. Spring 애노테이션 동작 원리 이해

    • @Autowired, @Controller, @Service 애노테이션이 어떻게 동작하는지.
    • Bean 생성 및 관리 과정을 직접 구현하며 학습.
  2. AOP(Aspect-Oriented Programming) 학습

    • 리플렉션을 통해 메서드 호출 전에 공통 로직을 삽입하는 AOP의 동작 원리를 이해.
  3. 프레임워크 흉내내기

    • 간단한 DI 컨테이너를 만들어 Spring의 기본 원리 학습.

결론

리플렉션은 신입 개발자가 직접 사용하는 일이 많지는 않지만, Spring과 같은 프레임워크의 동작 원리를 이해하고 문제 해결 능력을 기르는 데 필수적입니다. 위의 실습 예제를 통해 기본적인 개념과 활용 방법을 익히면 실무에서 직면할 프레임워크 이해에 큰 도움이 될 것입니다.

0개의 댓글