ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Core - 동적 프록시 (리플렉션)
    FrameWork/Spring&Spring-boot 2024. 3. 12. 14:20

    동적 프록시 - 리플렉션

    지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다. 그런데 문제는 대상 클래스 수 만큼 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.

     

    자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 쉽게 이야기해서 프록시 클래스를 이전의 예시에서 처럼 계속 만들지 않아도 된다는 것이다. 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어 낼 수 있다. 

     

    JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다. 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다. 앞으로 JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술을 알아보자.

     

    Relection Test

    package com.core.springproxy.jdkdynamic;
    
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    
    @Slf4j
    public class ReflectionTest {
    
        @Test
        void reflection0(){
            Hello target = new Hello();
    
            //공통 로직1 시작
            log.info("start");
            String result1 = target.callA(); //호출하는 메서드가 다르다.
            log.info("result1 = {}", result1);
            //공통 로직2 종료
    
            //공통 로직2 시작
            log.info("start");
            String result2 = target.callB(); //호출하는 메서드가 다르다.
            log.info("result2 = {}", result2);
            //공통 로직2 종료
    
        }
    
        @Slf4j
        static class Hello{
            public String callA(){
                log.info("callA");
                return "A";
            }
            public String callB(){
                log.info("callB");
                return "B";
            }
        }
    
    }
    

     

    • 공통 로직1과 공통 로직2는 호출하는 메서드만 다르고 전체 코드 흐름이 완전히 같다.
      • 먼저 start로그를 출력한다.
      • 어떤 메서드를 호출한다.
      • 메서드의 호출 결과를 로그로 출력한다.
    • 여기서 공통 로직1과 공통 로직2를 하나의 메서드로 뽑아서 합칠 수 있을까?
    • 쉬워 보이지만 메서드로 뽑아서 공통화하는 것은 생각보다 어렵다. 왜냐하면 중간에 호출하는 메서드가 다르기 때문이다.
    • 호출하는 메서드인 target.callA(), target.callB()이 부분만동적으로 처리할 수 있다면 문제를 해결할 수도 있을것 같다.

     

    log.info("start");
    String result = xxx(); //호출 대상이 다름, 동적 처리 필요
    log.info("result={}", result);

     

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

     

    ➕참고
    람다를 사용해서 공통화 하는 것도 가능하다. 하지만 여기서는 리플렉션 학습을 위해서 람다를 사용하기 어려운 상황이라고 가정하자.

     

     

    Relection - reflection1 추가

    @Test
    void reflection1() throws Exception {
        //클래스 정보
        Class classHello = Class.forName("com.core.springproxy.jdkdynamic.ReflectionTest$Hello");
    
        Hello targe = new Hello();
        //callA 메서드 정보
        //문자열 파라미터를 통해 메서드 정보를 얻기 때문에
        //파라미터를 변경 해서 언제든 다른 메서드 정보를 불러오도록 수정할 수 있다.
        Method methodCallA = classHello.getMethod("callA");
        //target에 있는 methodCallA를 호출하는것
        Object result1 = methodCallA.invoke(targe);
        log.info("result1={}",result1);
    
    
        Method methodCallB = classHello.getMethod("callB");
        //target에 있는 methodCallB를 호출하는것
        Object result2 = methodCallB.invoke(targe);
        log.info("result2={}",result2);
    
    }

     

    • Class.forName("com.core.springproxy.jdkdynamic.RelectionTest$Hello") : 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해 $를 사용한다.
    • classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
    • methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallA 는 Hello 클래스의 callA() 라는 메서드 메타정보이다. methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA()메서드를 찾아서 실행한다. 위 코드에서는 target의 callA() 메서드를 호출한다.

     

    그런데 target.callA()나 target.callB() 메서드를 직접 호출하면 되지 이렇게 메서드 정보를 획득해서 메서드를 호출하면 어떤 효과가 있을까? 여기서 중요한 핵심은 클래스나 메서 정보를 동적으로 변경할 수 있다는 점이다. 

     

     

    기존에 callA() , callB() 메서드를 직접 호출하는 부분이 Method로 대체되었다.(즉 메서드 코드가 추상화 되었다.) 덕분에 이제 공통 로직을 만들 수 있게 되었다.

     

     

    RelectionTest - reflection2추가 

    @Test
    void reflection2() throws Exception {
        //클래스 정보
        Class classHello = Class.forName("com.core.springproxy.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)를 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생한다.

     

    정리

    정적인 target.callA() , target.callB() 코드를 리플렉션을 사용해서 Method라는 메타정보로 추상화했다.덕분에 공통 로직을 만들 수 있게 되었다.

     

    주의 
    리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
    예를 들어 지금까지 살펴본 코드에서 getMethod("callA")안에 들어가는 문자를 실수로 getMethod("callZ")로 작성해도 컴파일 오류가 발생하지 않는다.대신 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다. 가장 좋은 오류는 개발자가 사전에 확인 할수 있는 컴파일 오류이고, 가장 나쁜 오류는 사용자가 직접 사용할때 발생하는 런타임 오류이다.

     

    따라서 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그램밍언어는 타입정보를 기반으로 컴파일 시점에 오류를 잡아 냄으로써 진보되어 왔는데, 리플렉션은 이것에 역행하는 방식이다. 

     

    따라서 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해아 한다.

     

     

    [출처 - 스프링 핵심원리-고급편, 저 김영한]

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

     

    스프링 핵심 원리 - 고급편 강의 - 인프런

    스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기 📢 수강

    www.inflearn.com

     

    댓글

Designed by Tistory.