프록시 기술의 한계
JDK 동적 프록시와 CGLIB를 사용해서 AOP 프록시를 만드는 방법에는 각각 장단점이 있다.
JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 기반으로 프록시를 생성한다.
CGLIB는 구체 클래스를 기반으로 프록시를 생성한다.
물론 인터페이스가 없고 구체 클래스만 있는 경우에는 CGLIB를 사용해야 한다. 인터페이스가 있는 경우에는 JDK 동적 프록시나 CGLIB 둘중에 하나를 선택할 수 있다.
ProxyFactory에 proxyTargetClass 옵션에 따라서 둘중 하나를 선택해서 프록시를 만들 수 있다.
- proxyTargetClass = false JDK 동적 프록시를 사용해서 인터페이스 기반 프록시 생성
- proxyTargetCalss = true [default] CGLIB를 사용해서 구체 클래스 기반 프록시 생성
- 옵션과 무관하게 인터페이스가 없으면 JDK 동적 프록시를 사용할 수 없으므로 CGLIB를 사용한다.
JDK 동적 프록시 한계
MeberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new FroxyFactory(target);
proxyFactory.setProxyTagrgetClass(false); //JDK 동적 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService meberService = (MemberService) proxyFactory.getProxy();
//프록시를 구현 클래스로 캐스팅 실패, ClassCastException 예외 발생
Assertions.assertThrows(ClassCastException, () -> {
MemberServiceImpl castingMemberService =
(MemberServiceImpl) meberServiceProxy;
}
)l;
여기서는 MeberServiceImpl 타입을 기반으로 JDK 동적 프록시를 생성했다. MemberServiceImpl 타입은 MemberService 인터페이스를 구현한다. 따라서 JDK 동적 프록시는 MemberService 인터페이스를 기반으로 프록시를 생성한다. 그런데 여기에서 JDK Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅 하려고 하니 예외가 발생한다. 왜냐하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문이다. JDK Proxy는 MemberService 인터페이스를 기반으로 생성도니 프록시이다. 따라서 JDK Proxy MemberSerivce로 캐스팅은 가능하지만 MemberServiceImpl 타입으로는 캐스팅이 불가능하다.
CGLIB 동적 프록시
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(tareget);;
proxyFactory.setProxyTargetClass(true); //CGLIB 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
//CGLIB 프록시를 구현 클래스로 캐스팅 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl)memberServiceProxy;
MemberSerivceImpl 타입을 기반으로 CGLIB 프록시를 생성했다. MemberServiceImpl 타입은 MemberService 인터페이스를 구현했다. CGLIB는 구체 클래스를 기반으로 프록시를 생성한다. 따라서 CGLIB는 MemberServiceImpl 구체 클래스를 기반으로 프록시를 생성한다. 그렇기 때문에 CGLIB Proxy를 대상 클래스인 MemberServiceImpl 타입으로 캐스팅하면 성공한다. 따라서 CGLIB Proxy는 MemberServiceImpl은 물론이고, MemberServiceImpl이 구현한 인터페이스인 MemberService로도 캐스팅할 수 있다.
프록시 기술과 한계 - 의존관계 주입
@Slf4j
@SpringBootTest(properties = {"spring.aop.proxy-target.class=false"}) //JDK동적 프록시
@SpringBootTest(properties = {"spring.aop.proxy-target.class=true"}) //CGLIB 프록시
public class proxyDITest {
@Autowired MemberSerivce memberSerivce;
@Autowired MemberServiceImpl memberServiceImpl;
@Test
void go() {
log.info("memberService class={}, memberService.getClass());
log.info("memberServiceImpl class={}, memberServiceImpl.getClass());
//memberSerivceImpl.hello("hello");
}
}
- @SpringBootTest(properties = {"spring.aop.proxy-target.class=false"}) : application.properties에 설정하는 대신에 해당 테스트에서만 설정을 임시로 적용한다.
- spring.aop.proxy-target.class=false : 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다.
문제 발생
spring.aop.proxy-target-class=false
실행결과
BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to
be of type 'hello.aop.member.MemberServiceImpl' but was actually of type
'com.sun.proxy.$Proxy54'
- @Autowired MemberService memberService : JDK Proxy는 MemberService 인터페이스를 기반으로 만들어진다. 따라서 해당 타입으로 캐스팅에는 문제가 없다.
- @Autowired MemberServiceImpl memberService : 문제는 여기서 발생한다. JDK Proxy는 MemberSerivce 인터페이스를 기반으로 만들어진다. 따라서 MemberServiceImpl 타입이 뭔지 전혀 모른다. 그래서 해당 타입에 주입할 수 없다.
spring.aop.proxy-target-class=true
- @Autowired MemberSerivce memberService : CGLIB Proxy는 MemberServiceImp 구체 클래스를 기반으로 만들어진다. MemberServiceImpl은 MemberService 인터페이스를 구현했기 때문에 해당 타입으로 캐스팅 할 수 있다.
- @Autowired MemberServiceImpl memberSerivceImpl : CGLIB Proxy는 MemberServiceImpl 구체 클래스를 기반으로 만들어진다. 따라서 해당 타입으로 캐스팅 할 수 있다.
정리
실제로 개발할 때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계 주입을 받는 것이 맞다.
DI의 장점은 DI 받는 클라이언트 코드의 변경 없이 구현 클래스를 변경할 수 있는 것이다. 이렇게 하려면 인터페이스를 기반으로 의존관계를 주입 받아야 한다. MemberServiceImpl 타입으로 의존관계 주입을 받는 것 처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할 때 의존관계 주입을 받는 클라이언트의 코드도 함께 변경해야 한다.
따라서 올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지는 않는다.
김영한님 강의를 듣고 정리