이전 포스팅에서 포인트컷 지시자 중 `this`와 `target`을 다루며 한 가지 주의사항을 이야기했었다.
JDK 동적 프록시는 인터페이스를 상속받은 프록시 객체를 스프링 빈으로 등록하기 때문에, *구체 클래스 타입으로 타입 캐스팅이 불가능하다는 점이다.*
당시에 "이런 유사한 이유로 스프링은 CGLIB를 기반으로 프록시 객체를 생성하도록 기본값을 설정했을까?" 의문을 가졌었는데 본 포스팅에서 자세히 이야기해보고자 한다.
1. JDK 동적 프록시와 CGLIB의 차이점
표면적인 차이점은 이미 이야기한 바와 같다.
JDK 동적 프록시의 경우, 인터페이스를 상속받은 프록시 객체가 생성되기 때문에 *자식 타입으로는 캐스팅이 불가능하다.*
반면, CGLIB의 경우 구체 클래스를 상속받은 프록시 객체가 생성되기 때문에 *부모, 자식 타입 상관없이 캐스팅이 가능하다.*
간단한 테스트 코드로 확인해보자.
@Slf4j
public class ProxyCastingTest {
@Test
void jdkProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(false); // JDK 동적 프록시 사용, 프록시 팩토리 + 인터페이스 기반을 사용하는 경우 기본 값이 false;
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
// JDK 동적프록시를 구현 클래스로 캐스팅 시도하면 실패, ClassCassException 발생
Assertions.assertThatThrownBy(() -> {
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}).isInstanceOf(ClassCastException.class);
}
@Test
void cglibTest() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // CGLIB 사용
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
// JDK 동적프록시를 구현 클래스로 캐스팅 시도하면 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
}
위 테스트의 경우 타입 캐스팅 예외를 잡도록 구성해 통과하겠지만 억지로 캐스팅을 시도하면 아래와 같은 예외가 발생한다.
차이점은 알겠다. 근데 실제로 캐스팅할 상황이 많지 않을텐데, 문제 상황이 무엇일까?
또한 CGLIB는 그럼 단점이 없을까?
2. JDK 동적 프록시의 단점
간단한 예제로 JDK 동적 프록시를 사용하는 스프링 AOP를 적용하는 상황을 만들어보자.
@Slf4j
@Aspect
public class ProxyDIAspect {
@Before("execution(* hello.aop..*.*(..))")
public void doTrace(JoinPoint joinPoint) {
log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
}
}
@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시
@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");
}
}
의존관계 주입을 부모 타입의 인터페이스, 그리고 구체 클래스 타입으로 각각 받아봤다.
실행 결과는 어떨까? 당연히 구체 클래스 타입으로는 받지 못해 예외가 발생할 것이다.
그렇지만 Dependency Injection은 인터페이스가 존재한다면 인터페이스를 기반으로 의존관계를 주입받는 것이 옳기에, 올바르게 설계된 애플리케이션의 경우 위와 같은 오류를 만날 상황은 많지 않을 것이다. 그렇다면 이런 특이 상황이 큰 단점은 아니지 않을까?
그럼에도 불구하고 테스트 코드를 작성할 때, 혹은 여러 특이 상황에서 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입 받아야하는 경우가 있을 수 있다. 이 경우 CGLIB를 통해 구체 클래스를 기반으로 AOP 프록시를 적용하면 해결 될 문제이다.
3. CGLIB의 단점
CGLIB는 구체 클래스를 상속 받기 때문에, 몇 가지 제약이 생긴다.
- 구체 클래스에 기본 생성자를 필수로 만들어야한다.
- 생성자를 두 번 호출하게 된다.
- 자바 상속의 단점으로, final 키워드를 사용하는 클래스, 메서드를 사용할 수 없다.
- 구체 클래스에 기본 생성자 필수
자바는 클래스 상속 관계가 있을 때, 자식 클래스의 생성자를 호출하면서 동시에 부모 클래스의 생성자도 호출해야한다.
생략을 하더라도, 기본적으로 super()가 호출되며 부모 클래스의 기본 생성자가 호출된다.
생성자가 하나도 없다면 자동으로 만들어지지만, 파라미터를 가지는 생성자가 존재한다면 별도로 기본 생성자를 생성해야한다.
- 생성자 두 번 호출 문제
생성자를 두 번 호출한다니, 무슨 말일까?
그림에서 보이듯, 프록시 객체를 생성할 때 부모의 생성자를 호출하면서, 실제 target 인스턴스를 생성할 때 역시 생성자를 호출하므로 두 번 호출하게 된다.
- final 키워드 문제
final 키워드가 클래스나 메서드에 있다면 상속이나 오버라이딩이 불가능하다. CGLIB는 클래스 상속을 기반으로 동작하기에 프록시가 생성되지 않거나 정상 동작하지 않을 수 있다.
하지만, 프레임워크를 개발하는게 아니라 일반적인 웹 애플리케이션을 개발한다면 클래스 혹은 메서드에 final 키워드를 잘 사용하지 않는다. 따라서 이 부분은 크게 문제가 되지 않는다.
4. 스프링의 선택
스프링은 우선 구체 클래스 타입의 캐스팅이 안되는 JDK 동적 프록시의 제약을 벗어나기 위해 CGLIB를 기본 프록시 생성 전략으로 선택했다.
그러면서 CGLIB에 존재하는 단점들을 다음과 같이 해결했다.
- 기본 생성자 필수 문제
스프링 4.0부터 `objenesis`라는 라이브러리를 사용해 기본 생성자 없이 객체 생성을 가능하게 했다.
이 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 한다.
- 생성자 2번 호출 문제
`objnenesis`라이브러리를 통해 역시 해결되었다. 이제 한 번만 호출된다.
- CGLIB 기본 사용
스프링 부트 2.0 버전부터 CGLIB를 기본 프록시 생성 전략으로 사용하도록 했다.
이를 통해 구체 클래스 타입으로 의존관계를 주입하는 문제를 해결했다.
물론 우리에게 자유가 있기 때문에 별도의 설정을 통해 JDK 동적 프록시도 사용 가능하다.
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 3 (0) | 2024.05.11 |
---|---|
[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 2 (0) | 2024.05.10 |
[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 1 (0) | 2024.05.10 |
[Spring] 스프링 AOP 예제 구현 (0) | 2024.05.09 |
[Spring] @RequestBody로 매핑하는 객체에 기본 생성자는 필수일까? (0) | 2024.05.05 |