오봉이와 함께하는 개발 블로그

스프링 핵심 원리 - 고급편 > 리플렉션 본문

BE/Spring

스프링 핵심 원리 - 고급편 > 리플렉션

오봉봉이 2024. 8. 28. 23:43
728x90

스프링 핵심 원리 - 고급편 > 리플렉션

이전 예제들을 보았을 때 로그 추적을 위한 프록시 클래스를 대상 클래스 수 만큼 만들어서 해결했다.
그리고 프록시 코드는 거의 비슷한 형태의 코드의 반복이었다.

자바가 기본적으로 제공하는 JDK 동적 프록시 기술이나, CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들 수 있다.
이로 인하여 프록시 클래스를 지금처럼 계속 찍어내지 않아도 된다. 프록시를 적용할 코드 하나만 생성하고 동적으로 주입하여 프록시를 찍어내기만 하면 된다.

JDK 동적 프록시를 사용하기 위해서 먼저 리플렉션을 알아야 하니 리플렉션의 최소한을 알아보자.

@Slf4j
public class ReflectionTest {

  @Test
  void reflection0() {
    Hello hello = new Hello();

    // 공통 로직 1 시작
    log.info("start");
    String result1 = hello.callA();
    log.info("result1 = {}", result1);
    // 공통 로직 1 종료

    // 공통 로직 2 시작
    log.info("start");
    String result2 = hello.callB();
    log.info("result2 = {}", result2);
    // 공통 로직 2 종료
  }

  @Slf4j
  static class Hello {
    public String callA() {
      log.info("Call A");
      return "A";
    }
    public String callB() {
      log.info("Call B");
      return "B";
    }
  }
}

위 코드에서 공통 로직 1공통 로직 2는 호출하는 메서드만 다르고 전체 흐름은 동일하다.
이때 하나의 메서드로 뽑아서 합칠 수 있을까? 하면 중간에 호출하는 메서드가 다르기 때문에 어려울 수 있다.
hello.callA()hello.callB()만 추상화 혹은 동적으로 처리만 되면 문제 해결이 가능해 보인다.

이럴 때 리플렉션을 통해 해결할 수 있다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.

먼저 리플렉션부터 보면

  @Test
  void reflection1() throws Exception {
    Class<?> classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

    Hello target = new Hello();
    Method methodCallA = classHello.getMethod("callA");
    Object result1 = methodCallA.invoke(target);
    log.info("result1 = {}", result1);

    Method methodCallB = classHello.getMethod("callB");
    Object result2 = methodCallB.invoke(target);
    log.info("result2 = {}", result2);
  }
  • Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello") : 클래스 메타정보를 획득 한다. 참고로 내부 클래스는 구분을 위해 $ 를 사용한다.
  • classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
  • methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallAHello 클래스의 callA() 이라는 메서드 메타정보이다. methodCallA.invoke(인스턴스) 를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메서드를 찾아서 실행한다. 여기서는 targetcallA() 메서드를 호출한다.

callAcallB를 직접 호출하지 않기에 methodCallA.invoke()와 같이 메서드를 호출하는 부분이 동일하게 처리가 가능해진다. 그렇다면 동적으로 변경도 가능하게 된다.

  @Test
  void reflection2() throws Exception {
    Class<?> classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
    Hello target = new Hello();

    Method methodCallA = classHello.getMethod("callA");
    dynamicCall(methodCallA, target);

    Method methodCallB = classHello.getMethod("callB");
    dynamicCall(methodCallB, target);
  }

  private void dynamicCall(Method method, Object target) throws Exception {
    log.info("start");
    Object result = method.invoke(target);
    log.info("result = {}", result);
  }
  • dynamicCall(Method method, Object target)
    • 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
    • Method method : 첫 번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메서 드 이름을 직접 호출했지만, 이제는 Method 라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공 된다.
    • Object target : 실제 실행할 인스턴스 정보가 넘어온다. 타입이 Object 라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론 method.invoke(target) 를 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생한다.

주의 사항

리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용하여 동적으로 만들 수 있지만, 리플렉션은 런타임에 작동하기 때문에 컴파일 시점에 오류를 잡을 수 없다.
오류 없는 프로그램이 없다고 하기에 가장 좋은 오류는 컴파일 오류다.(개발자가 개발 시점에 즉시 확인이 가능하기 때문...)
모든 배포가 끝나고 누군가가 실제로 실행하는 런타임 시점에 해당 오류가 검출된다면 상황이 좋지 않을 거 같다.

일반적으로 리플렉션은 사용하면 안 된다고 하며, 보통은 프레임워크나, 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용하도록 하자.

 

출처: 김영한 지식공유자의 스프링 핵심 원리 고급편

728x90
Comments