📍 리플렉션
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다.
프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.
리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
✔️ 주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다.
하지만 리플랙션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어 getMethod("callA")를 실수로 getMethod("callA2")로 작성해도 컴파일 오류가 발생하지 않는다. 대신 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다.
띠라서 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
📍 JDK 동적 프록시
지금까지 프록시를 적용하기 위해 적용 대상의 숫자 만큼 많은 프록시 클래스를 만들었다. 적용 대상이 100개면 프록시 클래스도 100개 만들었다. 그런데 앞서 살펴본 것과 같이 프록시 클래스의 기본 코드와 흐름은 거의 같고, 프록시를 어떤 대상에 적용하는가 정도만 차이가 있었다. 프록시의 로직은 같은데, 적용 대상만 차이가 있는 것이다.
✔️ JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수적이다.
new TimeInvocationHandler(target): 동적 프록시에 적용할 핸들러 로직
Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler)
- 동적 프록시는 java.lang.reflect.Proxy를 통해서 생성할 수 있다
- 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
실행 순서
1. 클라이언트는 JDK 동적 프록시의 call()을 실행한다
2. JDK 동적 프록시는 InvocationHandler.invoke()를 호출한다. TimeInvocationHandler가 구현체로 있으므로 TimeInvocationHandler.invoke()가 호출된다.
3. TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl)를 호출한다.
4. AImpl 인스턴스의 call()이 실행된다.
5. AImpl 인스턴스의 call()의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a"; }
}
public class BImpl implements BInterface {
@Override
public String call() { log.info("B 호출"); return "b";
} }
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@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;
} }
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());
}
}
AImpl, BImpl 각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler는 공통으로 사용했다.
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler만 만들어서 넣어주면 된다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
cf. 스프링이 제공하는 PatternMatchUtils.simpleMatch(..)를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다
- xxx: xxx가 정확히 매칭되면 참
- xxx*: xxx로 시작하면 참
- *xxx: xxx로 끝나면 참
- *xxx*: xxx가 있으면 참
- String[] patterns: 적용할 패턴은 생성자를 통해서 외부에서 받는다
📍 CGLIB (Code Generator Library)
CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다.
- Enhancer: CGLIB는 Enhancer를 사용해서 프록시를 생성한다
- enhancer.setSuperclass(ConcreteService.class): CGLIB는 구체 클래스를 상속받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속 받을지 지정한다.
- enhancer.setCallback(new TimeMethodInterceptor(target)): 프록시에 적용할 실행 로직을 할당한다.
- enhancer.create(): 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(ConcreteService.class)에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속(extends)해서 프록시를 만든다.
CGLIB 제약
- 부모 클래스의 생성자를 체크해야 한다 -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다
- 클래스에 final 키워드가 붙으면 상속 불가 -> CGLIB에서는 예외가 발생한다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다 -> CGLIB에서는 프록시 로직이 동작하지 않는다.
'스터디 > Spring' 카테고리의 다른 글
[SPRING 고급] 빈 후처리기 (0) | 2024.12.31 |
---|---|
[SPRING 고급] 프록시팩토리와 포인트컷,어드바이스,어드바이저 (1) | 2024.12.29 |
[SPRING 고급] 프록시 패턴과 데코레이터 패턴 (0) | 2024.12.24 |
[SPRING 고급] 템플릿 메서드와 전략 패턴, 템플릿 메서드 패턴 (1) | 2024.12.21 |
[SPRING 고급] 동시성 문제와 ThreadLocal 사용 (0) | 2024.12.17 |