[Java] Java Reflection이란?

NNIJGNUS·2024년 7월 12일

Java

목록 보기
1/4

오늘은 GPT와 함께 자바 리플렉션에 대해서 알아보도록 하자.

도와줘요, 지피티맨!

Java Reflection

🤖

자바 리플렉션은 런타임에 클래스와 객체의 정보를 검사하고 조작할 수 있는 기능입니다.

자바 리플렉션은 클래스와 객체의 정보를 검사하고 조작하는 Java의 기능이라고 한다.
자바 리플렉션은 어떤 방식으로 런타임에 클래스와 객체의 정보를 조작할 수 있을까?

🤖

클래스 객체 획득
Class.forName() 메서드나 클래스.class 문법을 통해 클래스 객체를 획득합니다.

메서드 정보 조회
클래스 객체의 getMethods(), getConstructors(), getFields() 등의 메서드를 사용하여 클래스의 메서드, 생성자, 필드 정보를 조회할 수 있습니다.

메서드 실행
메서드 객체의 invoke() 메서드를 사용하여 클래스의 메서드를 동적으로 호출할 수 있습니다.

필드접근
필드 객체의 get(), set() 메서드를 사용하여 필드의 값을 동적으로 읽거나 쓸 수 있습니다.

친절한 지피티맨은 자바 리플렉션이 어떤 방식으로 리플렉션이 객체에 접근하는지 알려줬다.
하지만 백문은 불여일견이라, 직접 눈으로 확인하도록 하자.
도와줘요, 인텔리제이맨!

간단한 클래스를 하나 만들었다.
클래스 내에 필드 값 namemoney을 가지며, 메소드로는 run()을 갖는다.

사용 예제

클래스 객체 얻기

우선 Class.forName(String className)으로 클래스 객체를 얻어올 수 있다.
이 클래스 객체는 해당 클래스의 필드, 메서드, 생성자 등 클래스의 메타 데이터를 포함한다.

객체 생성

getConstructor() 메서드를 사용하여 생성자 객체를 받아온다.
매개변수로는 생성자에 포함되는 매개변수의 클래스 객체를 넣어주면 된다.

이제 newInstanc() 메서드를 사용한다면 인스턴스를 생성할 수 있다.
매개변수로는 물론 생성자의 매개변수를 넣어주도록.

예제에서는 100원을 가진 가난한 존 도씨를 생성했다.

필드 접근 및 수정

필드를 접근하고 수정하기 해서는 getDeclaredField(String name) 메소드를 사용했다.
해당 클래스 객체에 존재하는 필드 명을 인자로 넣어주면 필드 객체를 반환해준다.

필드를 수정하기 위해서는 필드 객체의 메소드 setAccesible(true)를 호출해 수정가능하도록 한다.
예제에서는 가난한 존 도씨의 계좌에 백만원을 넣어줬다.

메소드 호출

getMethod(String methodName)을 호출하면 메소드 객체를 반환해준다.
invoke()를 호출해준다면 해당 메소드를 실행할 수 있다.
예제에서는 이제 부자가 된 존 도씨가 후다닥 달려가는 모습을 볼 수 있었다.

실행 결과

필드값이 정상적으로 수정되고, 메소드가 실행되는 것을 볼 수 있다.

그렇다면 왜?

런타임에 문자열로 객체를 생성하고 관리하는 것은 좀 신기하다.
하지만 우리는 이미 객체를 생성하고 관리하는 방법은 잘 알고 있는데, 리플렉션은 어디에 쓰이는걸까?
우선 단점부터 알아보도록 하자.

자바 리플렉션의 단점

🤖

성능 저하
리플렉션을 사용하면 일반적인 메서드 호출이나 필드 접근보다 더 많은 시간이 소요됩니다. 이는 리플렉션이 런타임에 다양한 검사를 수행해야 하기 때문입니다. 따라서, 성능이 중요한 애플리케이션에서는 리플렉션 사용이 성능 문제를 초래할 수 있습니다.

안전성 문제
리플렉션을 사용하면 컴파일 타임에 타입 체크를 할 수 없기 때문에, 런타임 오류가 발생할 가능성이 높아집니다. 예를 들어, 잘못된 메서드 이름이나 필드 이름을 사용하면 NoSuchMethodException이나 NoSuchFieldException이 발생할 수 있습니다.

보안 문제
리플렉션은 자바의 접근 제어자를 무시할 수 있기 때문에, 보안 위험이 있습니다. 리플렉션을 통해 private 필드나 메서드에 접근할 수 있어, 의도하지 않은 데이터 노출이나 무결성 문제가 발생할 수 있습니다. 보안 관리자가 활성화된 환경에서는 리플렉션 사용이 제한될 수 있습니다.

복잡성 증가
리플렉션을 사용하면 코드의 가독성과 유지보수성이 떨어질 수 있습니다. 리플렉션을 사용한 코드는 일반적인 코드보다 더 복잡하며, 디버깅과 문제 해결이 어려울 수 있습니다.

캡슐화 위반
리플렉션은 클래스의 캡슐화를 위반할 수 있습니다. 즉, 객체지향 프로그래밍의 기본 원칙 중 하나인 정보 은닉을 무시하고, 객체 내부의 세부 사항에 직접 접근할 수 있습니다. 이는 객체지향 설계의 장점을 약화시킬 수 있습니다.

신난 지피티맨이 열심히 자바의 뒷담화를 하는 모습을 볼 수 있었다.

그렇다면 성능도 좋지 않고, 런타임에 실행되어 안전성이 보장되지도 않고,
접근 제어자를 무시해 보안적으로도 문제가 있으며, 코드가 복잡해지기도 하면서
객체지향 설계를 무너뜨릴 수 있는 자바 리플렉션을 우리는 왜 공부해야 할까?

지피티 가라사대,

자바 리플렉션의 장점

🤖

동적 클래스 로딩 및 객체 생성
리플렉션을 사용하면 런타임에 클래스 이름을 동적으로 결정하고 해당 클래스를 로드하여 객체를 생성할 수 있습니다. 이는 컴파일 타임에 알 수 없는 클래스나 객체를 다룰 때 매우 유용합니다.

유연성 향상
리플렉션을 통해 런타임에 메서드 호출, 필드 접근, 생성자 호출 등을 동적으로 수행할 수 있습니다. 이로 인해 코드의 유연성이 증가하며, 다양한 상황에 적응할 수 있는 범용적인 코드를 작성할 수 있습니다.

프레임워크 및 라이브러리 개발에 유용
리플렉션은 스프링(Spring) 같은 프레임워크와 다양한 라이브러리에서 널리 사용됩니다. 예를 들어, 의존성 주입(Dependency Injection), 어노테이션 처리, 동적 프록시 생성 등 다양한 기능을 구현할 때 리플렉션이 필수적입니다.

어노테이션 처리
리플렉션을 사용하면 클래스, 메서드, 필드 등에 정의된 어노테이션 정보를 런타임에 조회하고 처리할 수 있습니다. 이는 어노테이션 기반의 설정과 구성을 가능하게 합니다.

디버깅 및 테스트 도구 개발
리플렉션은 디버깅 도구와 테스트 프레임워크를 개발하는 데 유용합니다. JUnit과 같은 테스트 프레임워크는 리플렉션을 사용하여 테스트 메서드를 자동으로 탐색하고 실행합니다.

아뿔싸! 자바 리플렉션이 없었다면 스프링도 없었던 것이었다.

과연 스프링에서는 어떤 방식으로 자바 리플렉션을 사용하는 것일까?

자바 리플렉션과 스프링

🤖

의존성 주입
스프링의 의존성 주입은 런타임에 객체의 의존성을 주입하는 기능으로, 리플렉션을 통해 이를 구현합니다. 스프링은 XML 설정 파일이나 어노테이션을 통해 설정된 빈(Bean)을 찾고, 리플렉션을 사용하여 객체를 생성하고, 필요한 의존성을 주입합니다.

AOP
스프링 AOP는 공통 관심사(예: 로깅, 트랜잭션 관리)를 분리하여 코드의 모듈화를 개선합니다. AOP는 리플렉션을 사용하여 런타임에 메서드 호출을 가로채고, 추가적인 기능을 수행합니다.

어노테이션 처리
스프링은 다양한 어노테이션을 사용하여 설정을 간소화합니다. 리플렉션은 이러한 어노테이션을 런타임에 검사하고 처리하는 데 사용됩니다.

빈 초기화와 소멸
스프링은 리플렉션을 사용하여 빈의 라이프사이클을 관리합니다. @PostConstruct와 @PreDestroy 어노테이션을 사용하면, 빈이 초기화될 때와 소멸될 때 실행할 메서드를 지정할 수 있습니다.

프록시 객체 생성
스프링은 프록시 객체를 생성하여 AOP와 같은 기능을 구현합니다. 프록시 객체는 원래 객체를 감싸고, 메서드 호출을 가로채어 추가적인 로직을 수행합니다. 리플렉션을 통해 프록시 객체는 원래 객체의 메서드와 필드에 접근합니다.

결론
스프링 프레임워크에서 리플렉션은 다양한 핵심 기능을 구현하는 데 필수적입니다. 의존성 주입, AOP, 어노테이션 처리, 빈 초기화 및 소멸, 프록시 객체 생성 등에서 리플렉션이 사용됩니다. 리플렉션을 통해 스프링은 런타임에 동적으로 클래스를 로드하고, 메서드와 필드에 접근하며, 애플리케이션의 유연성과 확장성을 제공합니다.

자바 리플렉션이 이렇게나 많은 일을 하다니! 여간 기합이 아닐수가 없다.

코드를 보면서 어떤 부분에서 쓰이는지 확인해보자.

의존성 주입

@Component
public class ExampleService {
    @Autowired
    private ExampleRepository repository;
    
    public void serve() {
        repository.save();
    }
}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ExampleService service = context.getBean(ExampleService.class);
        service.serve();
    }
}

여기서 @Autowired 어노테이션이 붙은 필드 repository는 의존성 주입의 대상이다.
스프링은 ExampleService 객체를 생성한 후 리플렉션을 사용하여 의존성 주입을 수행하게 된다.

AOP(Aspect-Orieted Programming)

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }
}

스프링 AOP는 공통 관심사(로깅, 트랜잭션 관리 등)를 분리하여 코드의 모듈화를 개선한다.
@Before어노테이션은 지정된 메소드가 호출되기 전에 logBefore 메소드를 실행하도록 한다.
이 때도 역시 스프링은 자바 리플렉션을 사용한다.

어노테이션 처리

@RestController
public class ExampleController {
    @GetMapping("/example")
    public String example() {
        return "Hello, World!";
    }
}

스프링은 어노테이션을 리플렉션을 통해 처리한다.
런타임에 스프링은 클래스와 메소드를 스캔하여 어노테이션을 확인하고, 해당 어노테이션에 맞는 처리 로직을 수행한다.

빈 초기화와 소멸

@Component
public class ExampleBean {
    @PostConstruct
    public void init() {
        System.out.println("Bean is initialized");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("Bean is destroyed");
    }
}

스프링은 리플렉션을 통해 @PostConstruct가 붙은 init 메소드를 빈 초기화 후 호출하고,
@PreDestroy가 붙은 destroy 메소드를 빈 소멸전에 호출하게 된다.

프록시 객체 생성

@Service
public class ExampleService {
    public void execute() {
        System.out.println("Executing service logic");
    }
}

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ExampleService service = context.getBean(ExampleService.class);
        service.execute();
    }
}

@EnableAspectJAutoProxy 어노테이션을 통해 스프링은 AOP 프록시 객체를 자동으로 생성하고, 원래 객체 대신 프록시 객체를 빈으로 사용한다.
프록시 객체는 리플렉션을 통해 객체의 메소드에 접근하여 추가 로직을 수행한다.

결론

사실은 리플렉션을 알아도 내가 직접 활용할 일은 크지 않을 것 같다.
그래도 우리 귀여운 스프링이 어떤 식으로 의존성을 주입하고

0개의 댓글