≣ 목차
스프링은 JDK 동적 프록시와 CGLIB를 사용하여 프록시 객체를 생성합니다. 이 두 기술은 각각의 한계점을 가지고 있습니다. 이에 대해 살펴보겠습니다. 또한, 프록시 객체의 내부 호출 시 발생할 수 있는 문제점에 대해서는 이번 포스팅에서는 다루지 않겠습니다.
JDK 동적 프록시 문제 - 타입 캐스팅
인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있습니다.
@Slf4j
public class ProxyCastingTest {
@Test
void jdkProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(false); // JDK 동적 프록시
// 프록시를 인터페이스로 타입 캐스팅 (memberServiceProxy = JDK Proxy)
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
log.info("proxy class={}", memberServiceProxy.getClass());
Assertions.assertThrows(ClassCastException.class, () -> {
MemberServiceImpl castingMemberService = (MemberServiceImpl)
memberServiceProxy;
});
}
}
해당 코드를 보면 MemberServiceImpl을 기반으로 JDK 동적 프록시를 생성했습니다. 참고로 MemberServiceImpl은 MemberService 인터페이스의 구현체입니다. 따라서 JDK 동적 프록시는 MemberService 인터페이스를 기반으로 프록시를 생성합니다.
이 프록시를 JDK Proxy 라고 하겠습니다.
그런데 JDK Proxy를 구체 클래스인 MemberServiceImpl으로 타입 캐스팅하면 예외가 발생합니다. 왜냐하면 JDK Proxy는 MemberService 인터페이스를 기반으로 생성되므로, MemberServiceImpl가 MemberService의 구현 클래스인지 알지 못합니다. 그래서 JDK Proxy를 MemberServiceImpl 타입으로 캐스팅을 시도하면 ClassCastException 예외가 발생합니다.
타입 캐스팅 문제는 CGLIB를 사용하면 해결됩니다.
@Slf4j
public class ProxyCastingTest {
@Test
void cglibProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true);//CGLIB 프록시
//프록시를 인터페이스로 캐스팅 성공 (memberServiceProxy = 프록시 객체)
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
log.info("proxy class={}", memberServiceProxy.getClass());
//CGLIB 프록시를 구현 클래스로 캐스팅 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
}
JDK 동적 프록시 문제 - 의존관계 주입
JDK 동적 프록시를 사용하면 의존관계 주입을 할 때 주의해야 합니다.
간단한 Aspect 코드
@Slf4j
@Aspect
public class ProxyDIAspect {
@Before("execution(* hello.aop..*.*(..))")
public void doTrace(JoinPoint joinPoint) {
log.info("proxyDIAspect");
}
}
@Slf4j
@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"}) //JDK 동적 프록시, DI 예외 발생
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"}) //CGLIB 프록시, 성공
@Import(ProxyDIAspect.class)
public class ProxyDITest {
@Autowired
MemberService memberService;
@Autowired
MemberServiceImpl memberServiceImpl;
@Test
void go() {
log.info("memberService class={}", memberService.getClass());
log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
memberServiceImpl.hello("hello");
}
}
JDK Proxy인 경우
- @Autowired MemberService memberService
- MemberService 인터페이스를 기반으로 생성되므로, 문제 없이 주입 가능.
- @Autowired MemberServiceImpl memberServiceImpl
- JDK 프록시를 사용하면 구체 클래스로의 타입 캐스팅이 발생합니다. MemberServiceImpl을 스프링 빈으로 등록할 경우, 빈을 해당 타입으로 캐스팅할 수 없으면 발생하는 BeanNotOfRequiredTypeException 예외가 발생합니다.
CGLIB Proxy인 경우에는 인터페이스, 구체 클래스로 타입 캐스팅으로 변경이 가능하므로 문제가 발생하지 않습니다.
CGLIB의 한계
1. 대상 클래스에 기본 생성자 필수
CGLIB는 대상 클래스를 상속하여 프록시를 생성하는데, 이 과정에서 자식 클래스의 생성자가 호출될 때 부모 클래스의 기본 생성자를 자동으로 호출해야 합니다. 대상 클래스에 기본 생성자(파라미터가 없는 생성자)가 반드시 필요하며, 만약 기본 생성자가 없다면 생성자가 하나도 없으면 자동으로 만들어집니다.
2. 생성자 2번 호출 문제
CGLIB는 프록시 객체를 생성할 때 실제 대상 객체와 프록시 객체를 모두 생성하는데, 이 과정에서 두 번 생성자가 호출되어 실제 대상 객체를 생성할 때와 프록시 객체를 생성할 때 부모 클래스의 생성자가 호출됩니다.
3. final 키워드 클래스, 메서드 사용 불가
final 키워드가 붙은 클래스나 메서드는 상속이나 오버라이딩이 불가능하므로 상속을 기반으로 작동하는 CGLIB는 정상적으로 작동할 수 없습니다.
프록시 기술과 한계 - 스프링의 해결책
JDK 동적 프록시 문제 해결
스프링 부트 2.0 버전부터 CGLIB를 기본으로 사용하도록 해서 구체 클래스 타입으로 의존관계를 주입하는 문제를 해결했습니다.
CGLIB 기본 생성자 필수 문제 , 생성자 2번 호출 문제 해결
스프링 프레임워크 4.0부터 objenesis라는 라이브러리를 사용해서 이 두 문제를 해결했습니다.
참고
[1] - 스프링 핵심 원리 고급편 - 김영한
'Spring' 카테고리의 다른 글
[Sprign Boot] Auto Configuration 정리 - @Conditaional (2) | 2024.10.10 |
---|---|
[Spring Boot + Java] JAR와 WAR 간단 정리 (1) | 2024.10.06 |
[Spring] Spring AOP 사용 시 내부 호출 문제 해결 방법 (0) | 2024.10.03 |
[Spring] 스프링 포인트컷 지시자 정리 (0) | 2024.10.01 |
[Spring] Spring AOP 사용 방법 - @Aspect 활용 (0) | 2024.09.27 |