스프링 핵심 원리 - 고급편 - 섹션5. 동적 프록시 기술
CS/김영한 스프링 강의

스프링 핵심 원리 - 고급편 - 섹션5. 동적 프록시 기술

동적 프록시를 이해하려면 일단 자바의 리플렉션 기술이 뭔지부터 알아야 한다.

 

@Slf4j
public class ReflectionTest {

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

        // 공통 로직1 시작
        log.info("start");
        String result1 = target.callA(); // 호출하는 메소드가 다름
        log.info("result1={}", result1);
        // 공통 로직1 끝

        // 공통 로직2 시작
        log.info("start");
        String result2 = target.callB(); // 호출하는 메소드가 다름
        log.info("result2={}", result2);
        // 공통 로직2 끝
    }

    @Test
    void reflection1() throws Exception {
        // 클래스 정보
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

        Hello target = new Hello();
        // callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        Object result1 = methodCallA.invoke(target);
        log.info("result1={}", result1);

        // callB 메서드 정보
        Method methodCallB = classHello.getMethod("callB");
        Object result2 = methodCallB.invoke(target);
        log.info("result2={}", result2);
    }

    @Test
    void reflection2() throws Exception {
        // 클래스 정보
        Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

        Hello target = new Hello();
        // callA 메서드 정보
        Method methodCallA = classHello.getMethod("callA");
        dynamicCall(methodCallA, target);

        // callB 메서드 정보
        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);
    }

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

}

 

함수를 다르게 부르는 코드 자체가 다를 때, reflection을 사용하면 런타임 시점에 동적으로 메소드를 변수로 정의해서 사용할 수 있다. 단점은 런타임에서 실행되기 때문에 컴파일에선 에러를 못잡는다. 딱 봐도 함수 이름을 문자열로 넘기는 것 부터... 그래서 왠만하면 쓰지 말고, 쓴다면 진짜 주의해서 써야 한다.

물론 람다를 넘길 수 있다. 근데 지금 리플렉션 학습하는 거니까..

 

 

jdk 동적 프록시를 알아보자. 일단 부가기능 없이 순수 비즈니스 기능만 만든다.

 

 

여기에 동적 프록시로 앞뒤로 추가기능을 붙일거다.

jdk 동적 프록시를 사용하려면 InvocationHandler라는걸 사용해야 한다.

 

Object가 동적 프록시할 대상(클래스), Method로 사용할 함수를 넘긴다. 애초에 이 클래스 인스턴스를 생성할 때 생성자로 어떤 클래스에 대해 적용할 지 대상을 줘야 한다. invoke로 하는게 아까 했던거. 인수도 넘길 수 있어서 args가 있음.

 

 

이렇게 해서 보니 전에는 Proxy용 클래스 각각 만들었었는데 이젠 InvocationHandler 클래스 하나만 만들고 여기에 타켓을 클래스로 넣는 방식으로 여러개를 만들 수 있었다. 출력된 클래스 이름을 보아도 각각 클래스를 만들어주었다.

 

 

이제 프록시용 클래스 하나 만들어 여기저기 주입 가능한걸 알았으니, 이걸 실제로 도입하면 된다.

 

 

 

 

똑같이 InvokationHandler만들고 우려먹으면 된다.

 

 

아직 남은 문제는, log를 안남기라고 하는 요청에 대해서도 남기는 것. 그냥 클래스에 쌩으로 앞뒤로 코드 추가한거라 그렇다.

 

그래서 프록시 필터 클래스에 필터 기능을 추가한다.

 

 

 

이것의 한계는 이거 쓰려면 v1처럼 인터페이스가 있어야 한다. 근데 v2처럼 인터페이스가 없는 경우에는 어떻게 쓰는가?

일반적인 방법으로는 어렵고 CGLIB라는 바이트 코드를 조작하는 특별한 라이브러리를 사용해야 한다.

 

 

CGLIB는 직접 사용할 일은 거의 없고, 나중에 @ProxyFactory를 사용할건데, 여기 안에서 사용되는거다.

일단 이게 뭔지 이해하기 위한 테스트로 인터페이스가 없는거 만들자

 

 

 

InvockationHandler를 상속받아 사용한 것처럼 MethodInterceptor를 상속받아 사용한다.

 

cglib에서 준게 Enhancer라는 건데, 이걸로 부모 클래스 지정 후 원하는 콜백 넣고 타입 변환해서 사용하면 된다.

 

 

아직 남은 단점은, 근데 프록시 쓸려면 두개 다 써야하나? 어차피 안의 코드 내용도 같은데? 싶다.

그래서 스프링이 제공하는 @ProxyFactory를 사용하면 해결된다.