인프런 강의 [스프링 핵심 원리 - 고급편] 강의를 기반으로 작성한 글 입니다.
[Spring] 프록시 (Proxy)에 대해서 알아보자
저번 시간에 프록시에 대해서 알아보았다.
두 클래스 사이에서 중간에서 기능을 수행하는 대리자 역할이라고 프록시를 설명했었다.
하지만 프록시의 단점이 무엇인가?
모든 프록시를 적용하려는 모든 클래스에 프록시 클래스를 생성해주어야 한다.
꽤나 큰 문제이다. 10가지의 클래스에 대해서 모든 프록시 클래스를 생성해주면 골치 아프다.
그래서 중복되는 기능은 하나의 프록시 클래스에서 묶을 수 있지 않을까?
그리고 하나의 클래스에서 프록시 구현체만 생성해줄 수 있지 않을까?
그것이 동적 프록시 기술이다.
일단 중간에서 기능을 대신 수행하는데, 그 기능을 수행하는 메소드가 각각 다른 것이다.
메소드 소요 시간 측정이라고 생각하자.
1. startTime 기록
2. 실제 메소드 동작
3. endTime 기록
4. resultTime = endTime - startTime
5. 소요 시간(resultTime) 출력
이 기능에서 중복되는 것들은 무엇인가? 1, 3, 4, 5번이다.
2번만 다르고 나머지는 다 동일하다.
그렇다면 이것을 하나의 함수로 묶어보겠다.
그렇다면 실제 로직 실행하는 부분에 함수 호출? 가능하다.
근데 여기서, 우리는 TimeResult로 메소드를 만들었다.
이 메소드를 호출하면서, 내가 원하는 로직을 실행하게 할 수 없을까??
함수에서 매개변수가 고작 String, Integer 정도지 메소드를 전달한 적이 있었나?
이 방식은 어려울 것이다. 이럴 때 사용할 수 있는 방법이 리플렉션과 동적 프록시이다.
클래스명으로 접근하여 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
을 통해 예외를 밖으로 내보내야 한다.
그래서 아래와 같은 방식도 가능하다.
호출하는 메소드 외부에서 Class와 method 정보를 알아놓고, 그 정보를 매개변수로 넘기는 방법이다.
그렇다면 리플렉션의 장단점은 무엇일까?
장점
단점
다시 말하지만, 프록시의 목적이 부가 기능을 추가하기 위함을 잊지말자.
예를 들면 로그도 출력하고 실행시간도 측정하는데, call( ) 메소드만 다르게 넣고 싶은 것이다.
그래서 우리는 JDK 동적 프록시 기술을 사용할 수 있다.
리플렉션은 메소드명(String)으로 값을 넘겨서 컴파일 시점에서 오류를 잡을 수 없는 상황을 JDK 동적 프록시로 해결할 수 있다.
하지만 JDK 동적 프록시 기술은 인터페이스에만 적용 가능하며, Handler를 추가적으로 만들어야한다.
간단한 로직으로 인터페이스와 구현체를 만들어보자.
📂 Ainterface.java
📂 AImpl.java | 📂 BImpl.java |
---|---|
![]() | ![]() |
이 클래스들은 실제 비즈니스 로직을 담고있다.
일단 부가기능을 추가하는 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());
}
이렇게 하면
TimeInvocationHandler
의 기능을 하는 새 프록시가 생성됨 proxy.Call( )
을 호출하면 TimeInvocationHandler
의 invoke
로 감method.invoke
에서 실제 로직인 Call
을 실행함.@Configuration
Annotation을 통해서 적용이 가능하다.
실제 실행할 클래스들의 구현체에 프록시를 적용시켜 프록시를 리턴해주면,
인스턴르를 생성할 때마다 프록시가 return되어 부가기능이 적용된 프록시 인스턴스들을 넘겨줄 수 있다.
또한 사용하는 것은 그냥 기존에 사용하던 것처럼 사용하면 된다.
만약 특정 메소드만 적용하고 싶다면 어떻게 할까?
그냥 핸들러에서 메소드 이름을 알아내고 조건문으로 판단하면 된다.
예를 들어, order...
로 시작하는 메소드 이름만 프록시를 적용할 수도 있다.
생성자는 다음과 같다.
1. 실제 실행할 클래스의 인스턴스
2. Log를 출력하는 기능을 하는 클래스
3. 실행하기 원하는 메소드의 패턴
핸들러에 패턴을 넘겨 사용할 수 있다.