일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자바의 정석 기초편 ch14
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 검증
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch13
- 스프링 고급 - 스프링 aop
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch8
- 코드로 시작하는 자바 첫걸음
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch1
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch5
- 스프링 mvc2 - 타임리프
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch4
- 스프링 입문(무료)
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch2
- 2024 정보처리기사 시나공 필기
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch11
- 게시글 목록 api
- 자바의 정석 기초편 ch6
- @Aspect
- 스프링 mvc2 - 로그인 처리
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch3
- Today
- Total
나구리의 개발공부기록
동적 프록시 기술, 리플렉션, JDK 동적 프록시(소개/예제/적용), CGLIB(소개/예제) 본문
동적 프록시 기술, 리플렉션, JDK 동적 프록시(소개/예제/적용), CGLIB(소개/예제)
소소한나구리 2024. 11. 9. 20:33출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 리플렉션
1) 리플렉션 이해
(1) 리플렉션과 동적프록시
- 지금까지 프록시를 사용하여 기존 코드를 변경하지 않고 로그 추적기라는 부가 기능을 적용할 수 있었으나 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점의 문제가 있음
- 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있어 프록시 클래스를 지금처럼 계속 만들지 않아도 됨
- 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 됨
- JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 알 수 있음
- 리플렉션 기술을 활용하면 클래스나 메서드의 메타정보를 동적으로 획득하고 코드도 동적으로 호출할 수 있음
- 해당 강의에서는 JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술만 알아볼 예정
(2) ReflectionTest - 예제 생성
- test 하위에 jdkdynamic 패키지를 생성 후 작성
- 공통로직1과 공통로직2는 호출하는 메서드만 다르고 전체 흐름이 완전히 같은데, 여기서 중복 제거를 하기 위해서 공통 로직1과 공통로직2를 하나의 메서드로 뽑아서 공통화하는 것은 생각보다 어려움
- 그 이유는 중간에 호출하는 메서드가 다르기 때문인데, 이 부분만 동적으로 처리할 수 있다면 문제를 해결할 수 있는데 이럴 때 사용하는 기술이 바로 리플렉션임
- 클래스나 메서드의 메타정보를 이용하여 동적으로 호출하는 메서드를 변경할 수 있음
** 참고
- 람다를 사용해서 공통화하는 것도 가능하지만 리플렉션 학습이 목적이기 때문에 지금 예제에서는 람다를 사용하기 어려운 상황이라고 가정하고 진행
package hello.proxy.jdkdynamic;
@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 종료
}
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
}
(3) reflection1 추가 및 실행
- Class.forName(...) : 클래스 메타 정보를 획득, 내부 클래스는 구분을 위해 $를 사용함
- classHello.getMethod(...) : 획득한 클래스 메타정보의 getMethod()메서드로 메서드에 대한 메타 정보를 획득, 예제에서는 문자열로 했으나 변수도 입력 가능함
- methodCallA.invoke(target) : 획득한 메서드의 메타정보의 invoke()메서드로 생성한 객체를 참조한 변수를 입력하여 실제 인스턴스의 메서드를 호출함
- 해당 예제에서는 methodCallA는 Hello 클래스의 callA()라는 메서드의 메타정보이고 methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주게 되면 해당 인스턴스의 callA() 메서드를 찾아서 실행하여 target의 callA() 메서드가 실행됨
- 해당 예제를 실행해보면 Hello 클래스의 callA()와 callB()가 정상적으로 출력됨
- 여기서의 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이며, 기존의 reflection0 테스트 케이스에서 직접 callA()와 callB() 메서드를 호출하는 부분이 Method라는 타입으로 추상화되어 공통로직을 적용할 수 있게 됨
@Test
void reflection1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 클래스 정보
Class<?> classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
// callA 메서드 정보
Method methodCallA = classHello.getMethod("callA"); // 변수도 입력 가능함
Object result1 = methodCallA.invoke(target); // methodCallA의 정보로 target에 있는 메서드를 호출
log.info("result1={}", result1);
// callB 메서드 정보
Method methodCallB = classHello.getMethod("callB"); // 변수도 입력 가능함
Object result2 = methodCallB.invoke(target); // methodCallB의 정보로 target에 있는 메서드를 호출
log.info("result2={}", result2);
}
(4) reflection2() 추가 및 실행
- reflection1 테스트케이스에서 획득한 각 메서드의 정보에서 invoke() 메서드를 통해 실제 객체의 메서드를 호출했다면, 이 부분을 메서드로 추상화하여 동적으로 처리할 수 있도록 변경
- dynamicCall(Method method, Object target) : 공통1, 공통2 로직을 한번에 처리할 수 있는 공통 처리 로직을 가진 메서드
- 첫 번째 파라미터에서 호출할 메서드 정보가 넘어오는 것이 핵심, Method라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공됨
- 두 번째 파라미터에서 실제 실행할 인스턴스 정보가 넘어오고, 타입을 Object로 하여 어떠한 인스턴스로 받을 수 있도록 작성
- 물론 method.invoke(target)을 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생함
- 실행해보면 해당 테스트도 정상적으로 모든 로직이 동작하는 출력결과를 확인할 수 있음
@Test
void reflection2() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> classHello = Class.forName("hello.proxy.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 InvocationTargetException, IllegalAccessException {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
/* 실행결과
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest -- start
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest$Hello -- callA
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest -- result=A
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest -- start
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest$Hello -- callB
16:03:02.087 [Test worker] INFO hello.proxy.jdkdynamic.ReflectionTest -- result=B
*/
(5) 정리 및 주의
- 정적인 target.callA(), target.callB() 코드를 리플렉션을 사용해서 Method라는 메타정보로 추상화하였고 덕분에 공통 로직을 만들 수 있게 되었음
- 리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있지만 리플렉션 기술을 런타임에 동작하기 때문에 컴파일 시점에 오류를 잡을 수 없음
- reflection1 테스트 케이스에서 getMethod("callA")안에 들어가는 문자를 전혀 관계없는 문자로 입력해도 컴파일 오류가 발생하지 않고 실행 후 해당 코드가 동작하는 시점에 오류가 발생하는 런타임 오류가 발생함
- 가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류인데, 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 편리하게 개발할 수 있던 것을 역행하는 방식이기 때문에 리플렉션은 일반적으로 사용하면 안됨
- 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야함
2. JDK 동적 프록시 - 소개
1) 소개
- 지금까지는 프록시를 적용하기 위해서는 적용 대상의 개수만큼의 프록시 클래스를 만들었어야 했음
- 프록시의 로직은 같은데 적용 대상만 차이가 있을 때 이렇게 프록시 클래스를 많이 생성하는 문제를 해결하는 것이 동적 프록시 기술임
- 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 되며 프록시 객체를 동적으로 런타임에 개발자 대신 만들어주고 동적 프록시에 원하는 실행 로직을 지정할 수 있음
** 주의
- JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어주기 때문에 인터페이스가 필수임
2) 기본 예제 코드
- test 하위에 생성한 jdkdynamic 패키지에 code 패키지를 만들어서 해당 예제들을 추가
(1) AInterface, AImpl, BInterface, BImpl
- 기능이 하나만있는 A,B인터페이스와 A,B구현체들
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
package hello.proxy.jdkdynamic.code;
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
3. JDK 동적 프록시 - 예제 코드
1) JDK 동적 프록시 InvocationHandler
(1) 프록시의 로직은 직접 구현
- 동적 프록시를 사용할때 프록시가 동작하는 로직은 별도로 작성해야하는데 InvocationHandler를 구현해서 작성해주면 됨
- proxy : 프록시 자신
- method: 호출한 메서드
- args : 메서드를 호출할 때 전달할 인수
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
(2) TimeInvocationHandler
- 기본 예제 코드가 위치한 ~.jdkdynamic.code 패키지에 작성
- InvocationHandler(import - java.lang.reflect)를 구현하여 JDK 동적 프록시에 적용할 공통 로직을 개발
- 기존에 작성한 로그 추적기와 동일한 로직이며 동적 프록시가 호출할 대상을 의존관계 주입을 받고 invoke()메서드를 오버라이딩
- method.invoke(target, args) : 리플렉션을 사용하여 target 인스턴스의 메서드를 실행하고 args로 메서드를 호출할 때 필요한 인수를 전달함
package hello.proxy.jdkdynamic.code;
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
(3) JdkDynamicProxyTest - 동적 프록시 사용
- new TimeInvocationHandler(target) : 동적 프록시에 적용할 핸들러 로직(프록시가 사용할 로직)을 생성
- java.lang.reflect.Proxy의 .newProxyInstance()메서드의 인수에 클래스 로더 정보, 인터페이스, 핸들러 로직을 입력하여 호출하면 두번째 인수로 입력한 인터페이스를 기반으로 동적 프록시를 생성하고 결과를 반환함
- 프록시를 생성할 기반 인터페이스를 여러개 입력할 수 있기에 배열로 입력
package hello.proxy.jdkdynamic;
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
// 프록시를 동적으로 생성하는 문법
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());
}
@Test
void dynamicB() {
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
// 프록시를 동적으로 생성하는 문법
BInterface proxy = (BInterface)Proxy.newProxyInstance(BInterface.class.getClassLoader(), // 클래스 로더 지정
new Class[]{BInterface.class}, // 프록시를 생성할 기반 지정
handler); // 프록시 로직
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
(4) 실행결과
- 출력 결과를 보면 프록시가 정상적으로 수행되어 A에서는 Proxy12가 B에서는 Proxy13이 동작한 것을 알 수 있음
- 프록시를 직접 만든것이 아니라 JDK 동적 프록시가 동적으로 만들어준 프록시이며 이 프록시는 TimeInvocationHandler로 직접 정의한 로직을 수행함
- 테스트를 각각 수행하면 동일한 프록시로 동작하지만 지금처럼 한번에 테스트를 실행해보면 JDK 동적 프록시가 각각 다른 동적 프록시 클래스를 만들어 주는 것을 확인할 수 있음
(5) 실행 순서 이해(dynamicA 테스트 기준)
- 클라이언트가 JDK 동적 프록시의 call()을 실행, 테스트 코드의 proxy.call();
- JDK 동적 프록시가 InvocationHandler.invoke()를 호출하고 TimeInvocationHandler가 구현체로 작성되어있기에 TimeInvocationHandler.invoke()가 호출됨
- TimeInvocationHandler가 내부 로직을 수행하고 method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl)를 호출
- AImpl 인스턴스의 call()이 실행됨
- AImpl 인스턴스의 call()의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아오고 시간 로그를 출력하고 결과를 반환함
(6) 정리
- AImpl, BImpl 각각의 프록시를 만들지 않았음에도 JDK 동적 프록시를 사용하여 동적으로 프록시를 만들고 TimeInvocationHandler는 공통으로 사용하였음
- JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 되며 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있게 됨
- 즉, 적용대상이 100개여도 동적 프록시를 통해서 생성하고 각각 필요한 InvocationHandler만 만들어서 넣어주면 됨
- 프록시를 클래스를 수 없이 만들어야하는 문제도 해결함과 동시에 부가 기능 로직도 하나의 클래스에 모음으로 인해 단일 책임 원칙(SRP)도 지킬 수 있게 되었음
(7) 클래스 및 런타임 객체 의존관계
- 점선은 개발자가 직접 만드는 클래스가 아님
4. JDK 동적 프록시 - 적용
1) V1 애플리케이션 적용
- JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있음
(2) LogTraceBasicHandler
- InvocationHandler 인터페이스를 구현한 클래스를 생성하면 JDK 동적 프록시에서 사용됨
- Object target으로 프록시가 호출할 대상을 의존관계 주입
- 기존 로그 추적기의 begin()메서드에는 각 계층에서 호출되었다는 메시지를 인수로 입력해야하는데, 파라미터로 넘어온 method를 통해 메시지를 생성하면 동적으로 파라미터로 넘어오는 method의 정보마다 메시지가 생성됨
- getDeclaringClass() : method가 속한 클래스 정보를 획득
- getSimpleName() : 획득한 클래스의 정보를 실제 클래스 이름만 반환(패키지 정보를 제거)
- getName() : method의 이름을 반환
package hello.proxy.config.v2_dynamicproxy.handler;
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 {
// begin()에 인수로 들어갈 메시지를 파라미터로 넘어온 method의 메타정보들로 꺼내서 입력
// getDeclaringClass().getSimpleNam() - 메서드가 속한 클래스 정보를 반환하고, 패키지 경로를 제외한 클래스의 이름을 반환
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;
}
}
}
(3) DynamicProxyBasicConfig
- 직접 개발한 프록시 클래스를 스프링 빈으로 등록하지 않고 JDK 동적 프록시 기술을 사용해서 각각의 Controller, Service, Repository에 맞는 동적 프록시를 생성해주면 됨
- 동적 프록시가 사용할 로그 추적기의 로직은 모든 계층에서 동일하기 때문에 LogTraceBasicHandler를 사용하며 각 계층마다 의존관계가 주입이 되기 때문에 new로 인스턴스를 생성해서 사용해야 함
package hello.proxy.config.v2_dynamicproxy;
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
// 프록시 생성
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace));
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
// 프록시 생성
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace));
return proxy;
}
@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;
}
}
(4) ProxyApplication 수정 및 실행
- 방금 생성한 설정파일을 @Import로 등록하고 애플리케이션으로 실행 후 매핑된 URL로 접속하여 요청파라미터를 보내면 정상적으로 로그가 동작하는 것을 확인할 수 있음
- 하지만 /v1/no-log로 접속했을 때에는 log를 남기지 말아야하는데 현재 상황에서는 동적프록시가 모두 동작하고 있어 요구사항에 맞춰 이부분을 로그가 남지 않도록 처리해야함
@Import(DynamicProxyBasicConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
// 기존 로직 동일
}
(5) 그림으로 정리
2) 메서드 이름 필터 기능 추가
- 요구사항에 의해 /v1/no-log로 접속했을 때 로그가 남지 않도록 문제를 해결
(1) LogTraceFilterHandler
- LogTraceBasicHandler에 특정 패턴일 경우에만 LogTrace가 동작하도록 필터를 적용한 핸들러 클래스를 생성
- 생성자를 통해 외부에서 입력받은 patterns 변수의 값이 method의 이름과 매칭하는 로직을 작성
- 스프링이 제공하는 PatternMatchUtils.simpleMatch(...)를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있음
package hello.proxy.config.v2_dynamicproxy.handler;
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);
}
// 로그남기는 로직은 동일
}
}
(2) DynamicProxyFilterConfig
- 기존에 작성한 BasicConfig와 거의 동일하며 filter로 사용할 상수 PATTERNS를 생성하여 request, order, save로 시작하는 패턴일 때만 로그가 동작하도록 설정
- 프록시 생성시 동작할 로직의 핸들러를 LogTraceFilterHandler가 동작하도록 모든 메서드를 변경
package hello.proxy.config.v2_dynamicproxy;
@Configuration
public class DynamicProxyFilterConfig {
// 패턴 정의
private static final String[] PATTERNS = new String[]{"request*", "order*", "save*"};
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
// 프록시 생성
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
// 필터가 적용된 핸들러로 프록시의 로직을 변경
new LogTraceFilterHandler(orderController, logTrace, PATTERNS));
return proxy;
}
// serivce, repository로 마찬가지로 LogTraceFilterHandler로 프록시를 생성
}
(3) ProxyApplication 수정 및 실행
- @Import(DynamicProxyFilterConfig.class)로 설정파일을 등록하고 실행해보면 /v1/no-log로 접속해보면 log가 남지않고 /v1/request?itemId=입력값에 접속했을 때에만 로그가 남도록 문제가 해결됨
(4) JDK 동적 프록시의 한계
- JDK 동적 프록시는 인터페이스가 필수이기에 V2 예제처럼 구체 클래스만 있는 경우에는 적용하기가 어려움
- 이런 경우에 동적 프록시를 적용하기 위해서는 CGLIB라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야함
5. CGLIB
1) 소개
(1) CGLIB: Code Generator Library
- 바이트 코드를 조작하여 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있음
- CGLIB는 원래 외부 라이브러리인데 스프링 프레임워크가 스프링 내부 소스 코드에 포함하여 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있음
- 우리가 CGLIB를 직접 사용하는 경우는 거의 없고 이후에 설명할 스프링의 ProxyFactory라는 것이 이 기술을 편리하게 사용하도록 도와주기 때문에 이번 글에서는 CGLIB가 무엇인지 대략적인 개념만 잡고 넘어가면 됨
2) 공통 예제 코드
- 인터페이스와 구현이 있는 서비스 클래스와 구체 클래스만 있는 서비스 클래스를 생성하여 공통으로 사용할 예제 코드를 생성
- test 하위에 common.service 패키지를 생성하여 코드들을 작성
(1) ServiceInterface, ServiceImpl
- 인터페이스와 구현이 있는 서비스 클래스
package hello.proxy.common.service;
public interface ServiceInterface {
void save();
void find();
}
package hello.proxy.common.service;
@Slf4j
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
log.info("save 호출");
}
@Override
public void find() {
log.info("find 호출");
}
}
(2) ConcreteService
- 구체 클래스만 있는 서비스 클래스
package hello.proxy.common.service;
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
3) CGLIB - 예제 코드
- JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler를 제공했듯이 CGLIB는 MethodInterceptor를 제공함
(1) MethodInterceptor
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
(2) TimeMethodInterceptor
- test 하위의 cglib.code 패키지를 생성하여 코드를 작성
- MethodInterceptor의 Import는 springframework로 해야함
- JDK 동적 프록시를 테스트할 때 생성한 TimeInvocationHandler와 거의 동일하며 invoke()메서드를 호출하는 대상이 method가 아니라 MethodProxy에서 invoke()메서드를 호출함
- CGLIB 매뉴얼에서 MethodProxy를 사용하는것을 권장한다고 함(상세한 내부 로직은 사실 크게 알 필요가 없음)
package hello.proxy.cglib.code;
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
// target 생성 및 생성자 코드 동일
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 기존 로직 동일
// 마지막 파라미터인 MethodProxy를 사용하면 조금더 동작이 빠르다고 매뉴얼에 설명되어있음
Object result = proxy.invoke(target, args);
// 기존 로직 동일
}
}
(3) CglibTest, CGLIB 사용 및 실행
- 인터페이스가 없는 ConcreteService를 target으로 CGLIB를 사용하여 프록시를 생성하는 테스트
- new Enhancer(): CGLIB를 생성하기 위해서는 Enhancer를 사용해야함
- enhancer.setSuperclass(...) : CGLIB는 구체 클래스를 상속 받아서 프록시를 생성하기 때문에 어떤 구체클래스를 상속 받을지 지정해야함
- enhancer.setCallback(...) : 프록시에 적용할 실행 로직을 할당
- enhancer.create() : CGLIB로 동작하는 프록시를 생성, setSuperclass()에서 지정한 클래스를 상속받아서 프록시가 만들어짐
- 실행 후 생성한 proxy의 클래스를 확인해보면 enhancerByCGLIB$$임의코드로 로그가 출력된 것이 확인되는데 이것이 CGLIB가 생성한 프록시라는 뜻이며 CGLIB가 동적으로 클래스를 생성할 때 이러한 규칙으로 이름을 생성함
** 참고
- JDK 동적 프록시는 인터페이스를 구현해서 프록시를 만듦
- CGLIB는 구체 클래스를 상속해서 프록시를 만듦
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
// CGLIB 생성하는 코드
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class); // 상속받을 구체 클래스 지정
enhancer.setCallback(new TimeMethodInterceptor(target)); // 프록시에 적용할 실행 로직
ConcreteService proxy = (ConcreteService) enhancer.create();// 프록시 생성
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
}
/* 실행 로그
20:12:22.618 [Test worker] INFO hello.proxy.cglib.CglibTest -- targetClass=class hello.proxy.common.service.ConcreteService
20:12:22.620 [Test worker] INFO hello.proxy.cglib.CglibTest -- proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$48bd19d7
20:12:22.620 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor -- TimeProxy 실행
20:12:22.628 [Test worker] INFO hello.proxy.common.service.ConcreteService -- ConcreteService 호출
20:12:22.628 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor -- TimeProxy 종료 resultTime=7
*/
(4) 그림으로 정리
- JDK 동적 프록시와 구조가 크게 다른것이 없어 그림을 이해하기가 어렵지 않음
(5) CGLIB의 제약
- CGLIB는 상속을 사용하기 때문에 상속을 사용할 때 따라오는 제약이 존재함
- 부모 클래스의 생성자를 체크해야하고 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요함
- 클래스에 final 키워드가 붙으면 상속할 수가 없어서 예외가 발생함
- 메서드에 final 키워드가 붙으면 오버라이딩 할 수가 없어서 프록시 로직이 동작하지 않음
** 참고
- CGLIB를 사용하면 인터페이스가 없는 V2 애플리케이션에 동적 프록시를 적용할 수 있으나 바로 적용하기에는 제약이 몇가지 있음
- V2 애플리케이션에 기본 생성자를 추가하고 의존관계를 setter를 사용해서 주입하면 CGLIB를 적용할 수는 있으나 ProxyFactory를 통해서 CGLIB를 적용하면 이런 단점을 해결하고 더 편리하게 적용할 수 있기에 ProxyFactory를 배우면서 애플리케이션에 CGLIB를 프록시로 적용할 예정임