[Spring] 동적 프록시 기술

전현준·2024년 8월 30일
1

Spring

목록 보기
14/17
post-thumbnail

개요


인프런 강의 [스프링 핵심 원리 - 고급편] 강의를 기반으로 작성한 글 입니다.


프록시


[Spring] 프록시 (Proxy)에 대해서 알아보자

저번 시간에 프록시에 대해서 알아보았다.
두 클래스 사이에서 중간에서 기능을 수행하는 대리자 역할이라고 프록시를 설명했었다.

하지만 프록시의 단점이 무엇인가?

모든 프록시를 적용하려는 모든 클래스에 프록시 클래스를 생성해주어야 한다.

꽤나 큰 문제이다. 10가지의 클래스에 대해서 모든 프록시 클래스를 생성해주면 골치 아프다.


그래서 중복되는 기능은 하나의 프록시 클래스에서 묶을 수 있지 않을까?

그리고 하나의 클래스에서 프록시 구현체만 생성해줄 수 있지 않을까?

그것이 동적 프록시 기술이다.


프록시를 다양한 기능에 어떻게 적용할까?


일단 중간에서 기능을 대신 수행하는데, 그 기능을 수행하는 메소드가 각각 다른 것이다.

메소드 소요 시간 측정이라고 생각하자.

1. startTime 기록
2. 실제 메소드 동작
3. endTime 기록
4. resultTime = endTime - startTime
5. 소요 시간(resultTime) 출력

이 기능에서 중복되는 것들은 무엇인가? 1, 3, 4, 5번이다.
2번만 다르고 나머지는 다 동일하다.

그렇다면 이것을 하나의 함수로 묶어보겠다.

그렇다면 실제 로직 실행하는 부분에 함수 호출? 가능하다.

근데 여기서, 우리는 TimeResult로 메소드를 만들었다.

이 메소드를 호출하면서, 내가 원하는 로직을 실행하게 할 수 없을까??

함수에서 매개변수가 고작 String, Integer 정도지 메소드를 전달한 적이 있었나?

이 방식은 어려울 것이다. 이럴 때 사용할 수 있는 방법이 리플렉션동적 프록시이다.



Reflection (리플렉션)


클래스명으로 접근하여 Method를 실행시키는 방식.
클래스명을 매개변수로 주어, 정보를 얻어낼 수 있다.

📂 hello.proxy.jdkdynamic.ReflectionTest

내부에 static으로 Hello 클래스를 생성해보자.

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

        public String callB(){
            log.info("callB");
            return "B";
        }
    }
 }

그리고 나서, 외부 메소드에서 클래스명으로 클래스 정보를 얻을 수 있다.

Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

Class.forName을 이용하여 클래스의 정보를 얻어내 보자.
ReflectionTest 클래스에서 Hello 내부 클래스의 정보를 얻어낼 수 있다.

클래스 정보를 얻어왔으니 이젠 메소드 정보를 얻어올 차례이다.

Method methodCallA = classHello.getMethod("callA");

callA라는 메소드 이름만 알아내면 메소드의 정보도 얻어낼 수 있는 것이다!


그렇다면 이렇게 알아낸 메소드로 실행해보자.

Hello target = new Hello();
Object result1 = methodCallA.invoke(target);

실제로 실행하려면 Hello 객체가 있어야 실행이 가능하다.
Hello 객체를 생성하고, 매개변수로 넣어 invoke 함수를 실행하면 실제 Hello.callA()를 실행 가능하다.


그렇다면 아까 우리가 처음에 얘기 했던 시간 측정 메소드도 메소드 이름으로 실행할 수 있는 것이다.

public Object TimeResult(String methodName) throws Exception{
    Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

	// method 명으로 메소드 정보를 알아낼 수 있음
    Method methodCallA = classHello.getMethod(methodName);

    Hello target = new Hello();
    
    // 알아낸 메소드 정보를 invoke 메소드를 통해서 실행시킬 수 있음 
    Object result1 = methodCallA.invoke(target);
	return result1;
}

실제 실행이 되며, callA의 메소드의 return 값은 result1에 담긴다.

추가적으로, throws Exception을 통해 예외를 밖으로 내보내야 한다.

그래서 아래와 같은 방식도 가능하다.

호출하는 메소드 외부에서 Classmethod 정보를 알아놓고, 그 정보를 매개변수로 넘기는 방법이다.


그렇다면 리플렉션의 장단점은 무엇일까?

  • 장점

    • 클래스명메소드명을 알면, 실행할 수 있다.
    • String 매개변수로 넘겨주면 된다!
    • 그렇기 때문에 템플릿에 메소드 일부만 변경하여 템플릿처럼 실행할 수 있다.
  • 단점

    • String 으로 넘기기 때문에, 클래스명이나 메소드명에 오타가 있어도 잡아내지 못한다.
    • 실제로 실행하는 런타임 시점에 오류가 발생하기 때문에, 서비스 동작 시 더 큰 문제로 발생할 수 있다.



동적 프록시


다시 말하지만, 프록시의 목적이 부가 기능을 추가하기 위함을 잊지말자.

예를 들면 로그도 출력하고 실행시간도 측정하는데, call( ) 메소드만 다르게 넣고 싶은 것이다.


그래서 우리는 JDK 동적 프록시 기술을 사용할 수 있다.

리플렉션은 메소드명(String)으로 값을 넘겨서 컴파일 시점에서 오류를 잡을 수 없는 상황을 JDK 동적 프록시로 해결할 수 있다.

하지만 JDK 동적 프록시 기술은 인터페이스에만 적용 가능하며, Handler를 추가적으로 만들어야한다.


인터페이스와 구현체

간단한 로직으로 인터페이스구현체를 만들어보자.

📂 Ainterface.java

📂 AImpl.java📂 BImpl.java

이 클래스들은 실제 비즈니스 로직을 담고있다.


부가기능 만들기 (Handler)

일단 부가기능을 추가하는 Proxy를 만들기 위해서 Handler를 만들어야한다.

이때 InvocationHander를 구현하여 만든다.

그리고 생성자에 실제로 실행할 클래스(AImpl.java, BImpl.java)의 객체를 전달하면 된다.

그리고 실제 실행할 부가 기능은 invoke 메소드를 Override하여 작성해두면 된다.

부가 기능들을 쭉 적고, 실제 실행할 메소드 (AImpl.call( ) / BImpl.call( ))은
6번째 line method.invoke( )에서 실행될 것이다.

Reflection과 동일하게, invoke의 파라미터는 다음과 같다.

invoke 매개변수
1. 실제 생성한 클래스의 인스턴스
2. 실제 실행할 비즈니스 로직의 메소드에 필요한 인자를 넣을 수 있다.


프록시를 적용하여 실행

    @Test
    void dynamicA(){
        // TimeInvocationHandler 생성자는 인스턴스가 필요
        Ainterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        // 인터페이스의 어느 클래스 로더에 할지, 어디 인터페이스 클래스에?, 어떤 로직을 (handler)
        Ainterface proxy = (Ainterface) Proxy.newProxyInstance(Ainterface.class.getClassLoader(), new Class[]{Ainterface.class}, handler);

        proxy.Call();
        log.info("targetClass={}",target.getClass());
        log.info("proxyClass={}",proxy.getClass());
    }

이렇게 하면

  1. TimeInvocationHandler의 기능을 하는 새 프록시가 생성됨
  2. proxy.Call( )을 호출하면 TimeInvocationHandlerinvoke로 감
  3. 내부의 method.invoke에서 실제 로직인 Call을 실행함.

실제로 Bean에 적용하려면?

@Configuration Annotation을 통해서 적용이 가능하다.

실제 실행할 클래스들의 구현체에 프록시를 적용시켜 프록시를 리턴해주면,
인스턴르를 생성할 때마다 프록시가 return되어 부가기능이 적용된 프록시 인스턴스들을 넘겨줄 수 있다.

또한 사용하는 것은 그냥 기존에 사용하던 것처럼 사용하면 된다.


Filter를 적용하고 싶다면?

만약 특정 메소드만 적용하고 싶다면 어떻게 할까?

그냥 핸들러에서 메소드 이름을 알아내고 조건문으로 판단하면 된다.

  • 부가기능 실행을 원하는 메소드 : 부가기능 + 기존 기능
  • 부가기능 실행을 원하지 않는 메소드 : 기존 기능만 실행

예를 들어, order...로 시작하는 메소드 이름만 프록시를 적용할 수도 있다.

생성자는 다음과 같다.
1. 실제 실행할 클래스의 인스턴스
2. Log를 출력하는 기능을 하는 클래스
3. 실행하기 원하는 메소드의 패턴

핸들러에 패턴을 넘겨 사용할 수 있다.

profile
백엔드 개발자 전현준입니다.

0개의 댓글

관련 채용 정보