스프링 핵심 원리 - 고급편 - (18) 동적 프록시 기술 - JDK 동적 프록시 - 적용
인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.
JDK 동적 프록시 - 적용 1
JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있다.
먼저 LogTrace를 적용할 수 있는 InvocationHandler를 만들자.

해당 v2_dynamicproxy.handler에 LogTraceBasicHandler를 만들었다.
LogTraceBasicHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LogTraceBasicHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
public LogTraceBasicHandler(Object target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() +
"." + method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
LogTraceBasicHandler는InvocationHandler인터페이스를 구현해서 JDK 동적 프록시에서 사용된다.private final Object target: 프록시가 호출할 대상이다. 즉 핵심 로직String message = method.getDeclaringClass().getSimpleName() + "." ...LogTrace에 사용할 메시지이다. 프록시를 직접 개발할 때는"OrderController.request()"와 같이 메시지를 직접 남겨줬다.- 이제는
Method를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용해서 만들어 주면 된다.
동적 프록시를 사용하도록 수동 빈 등록을 설정하자.
hello.proxy.config.v2_dynamicproxy.DynamicProxyBasicConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
return (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace));
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
return (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace));
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceBasicHandler(orderRepository, logTrace));
return proxy;
}
}
- 이전에는 프록시 클래스를 직접 개발했지만, 이제는 JDK 동적 프록시 기술을 사용해서 각각의
Controller,Service,Repository에 맞는 동적 프록시를 생성해주면 된다. LogTraceBasicHandler: 동적 프록시를 만들더라도LogTrace를 출력하는 로직은 모두 같기 때문에 프록시는 모두LogTraceBasicHandler를 사용한다.
ProxyApplication - 수정
이제 만든 config를 등록하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//@Import(AppV1Config.class)
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
//@Import(ConcreteProxyConfig.class)
@Import(DynamicProxyBasicConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
이제 실행해보자.

잘 된다.
정리

동적으로 프록시를 구현하지 않는다면, 컨트롤러 인터페이스에 관한 프록시를 만들고, 같은 역할을 하는 서비스 인터페이스에 관련된 프록시를 만들어야 한다.

이젠 컨트롤러, 서비스, 리포지토리 모두 InvocationHandler를 사용해서 동적으로 프록시를 만들어서 사용하게 된다.

런타임 흐름을 보면 다음과 같다.
하지만 문제가 하나 있다. 로그를 남기면 안되는/no-log도 호출해보면

no-log도 로그가 남아버린다.
JDK 동적 프록시 - 적용 2
메서드 이름 필터 기능 추가
- http://localhost:8080/v1/no-log
이 url에서는 로그 추적기가 작동해서는 안된다.
이런 문제를 해결하기 위해 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자.
hello.proxy.config.v2_dynamicproxy.handler.LogTraceFilterHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class LogTraceFilterHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
this.target = target;
this.logTrace = logTrace;
this.patterns = patterns;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 메서드 이름 필터
String methodName = method.getName();
// save, request, reque*, *est
if (!PatternMatchUtils.simpleMatch(patterns, methodName))
return method.invoke(target, args);
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() +
"." + method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
간단하게 기능이 추가되었다.
private final String[] patterns: 적용할 패턴을 생성자를 통해 외부에서 받는다.PatternMatchUtils.simpleMatch(..): 스프링이 제공하는 문자열 매칭 로직xxx: xxx가 정확히 매칭되면 참xxx*: xxx로 시작하면 참*xxx: xxx로 끝나면 참*xxx*: 문자열 안에 xxx가 있으면 참
- 이 기능을 이용해서 문자열 매칭이 안되면 바로
return method.invoke(target, args)- return null 이러면 메서드 실행 안돼버린다. 그냥 멈춰버린다.
이제 이 기능을 사용하기 위해 config 파일을 만들자.
hello.proxy.config.v2_dynamicproxy.DynamicProxyFilterConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
public class DynamicProxyFilterConfig {
private static final String[] PATTERNS = {"request*", "order*", "save*"};
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
return (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(orderController, logTrace, PATTERNS));
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
return (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceFilterHandler(orderService, logTrace, PATTERNS));
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS));
return proxy;
}
}
다른 차이는 크게 없고 LogTraceFilterHandler(..) 를 선언하고 패턴도 넘겨준다.
1
private static final String[] PATTERNS = {"request*", "order*", "save*"};
request, order, save로 시작하는 메서드가 호출될 때 로그를 남긴다.

이제 no-log 호출해도 로그가 남지 않는다.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이다.
그렇다면 V2 애플리케이션처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 CGLIB라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.
댓글남기기