본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
1. 프록시의 내부 호출
스프링의 프록시 방식의 AOP는 메서드 내부 호출(self-invocation)에 프록시를 적용할 수 없다는 한계가 있다. 프록시 방식의 AOP는 프록시를 통해서 대상 객체(target)를 호출해야만 AOP가 적용된다. 프록시에서 먼저 어드바이스를 호출한 후 대상 객체를 호출하는 것이다. 그러나 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하게 되므로 AOP가 적용되지 않고 어드바이스도 호출하지 않는다. 내부 호출이 발생하는 예제를 살펴보자.
CallServiceV0 - 내부 호출이 발생
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); //내부 메서드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
CallLogAspect - AOP를 적용하기 위한 Aspect
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* hello.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
CallServiceV0Test 예시
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV0Test {
@Autowired CallServiceV0 callServiceV0;
@Test
void external() {
callServiceV0.external();
}
@Test
void internal() {
callServiceV0.internal();
}
}
external() 테스트 메서드 실행 결과

- external()에는 AOP가 적용됐지만, internal()에는 AOP가 적용되지 않았다.
internal() 테스트 메서드 실행 결과

- internal()에 AOP가 적용되었다.
위와 같은 결과가 나온 까닭은 자바의 특성과 관련있다. internal()과 같이 자바에서 메서드 앞에 별도의 참조가 없으면 자기 자신의 인스턴스(this)의 메서드를 호출한다. 즉 this.internal()이 되는데, 여기서 this는 실제 대상 객체(target)이므로 이러한 형태의 내부 호출은 프록시를 거치지 않은 호출이 된다. 따라서 어드바이스가 호출되지 않고 AOP가 적용되지 않는 것이다.

2. 프록시 내부 호출 대안
**스프링 2.6부터는 자기 자신/세터 주입을 막아놓았다고 한다...
1) 대안1 - 자기 자신 주입
가장 단순한 해결 방법은 자기 자신을 주입받는 것이다. 생성자 주입으로는 자기 자신을 주입받을 수 없으므로 여기서는 수정자 주입을 사용했다.
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal(); //외부 메서드 호출
}
public void internal() {
log.info("call internal");
}
}
- 순환 참조 문제가 발생하므로 생성자 주입은 안 되고, 대신 setter로 의존성을 주입받는다 (수정자 주입)
자기 자신을 주입받으면 내부 호출이 일어나더라도, 호출 대상 또한 프록시이므로 AOP가 잘 적용된다. 다만 일반적인 생성자 주입으로는 순환 참조 문제로 인해 애플리케이션 실행이 되지 않는다는 점을 주의하자.

2) 대안2 - 지연 조회
자기 자신을 주입받는 대신 지연 조회를 사용하는 방법도 있다. 스프링 빈을 지연 조회하기 위해선 ApplicationContext나 ObjectProvider을 사용하면 된다.
@Slf4j
@Component
public class CallServiceV2 {
private final ObjectProvider<CallServiceV2> callServiceProvider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
log.info("call external");
CallServiceV2 callServiceV2 = callServiceProvider.getObject();
callServiceV2.internal(); //외부 메서드 호출
}
public void internal() {
log.info("call internal");
}
}
- ObjectProvider를 사용하여 스프링 컨테이너에서 빈을 조회한다
- 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점에 객체를 스프링 컨테이너에서 조회할 수 있다 (지연조회)
3) 대안3 - 구조 변경
앞선 해결방법1과 해결방법2는 자기 자신이나 Provider를 주입받아야 한다는 점에서 다소 어색한 감이 있다. 내부 호출 자체가 발생하지 않도록 구조를 변경하는 것이 가장 권장되는 해결방법이다.
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {
private final InternalService internalService;
public void external() {
log.info("call external");
internalService.internal(); //외부 메서드 호출
}
}
@Slf4j
@Component
public class InternalService {
public void internal() {
log.info("call internal");
}
}
- 내부 호출할 메서드를 별도의 객체로 옮기고, 해당 객체를 주입받는 식으로 구조가 변경되었다.
별도의 서비스로 분리하는 방법 말고도 클라이언트에서 external(), internal() 메서드를 각각 호출하는 방식으로도 구조를 변경할 수 있다(external() 메서드가 internal() 메서드를 호출하지 않는다는 가정하에). 또한, AOP는 public 메서드에만 적용되므로 내부 호출 문제는 public 메서드에서 public 메서드를 내부 호출하는 경우에만 발생한다.

2. JDK 동적 프록시의 한계
1) 타입 캐스팅
인터페이스가 없고 구체 클래스만 있는 경우엔 CGLIB를 사용해서 프록시를 생성해야 하지만, 인터페이스가 있는 경우엔 JDK 동적 프록시와 CGLIB 둘 중에서 하나를 선택할 수 있다. 인터페이스를 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능하다는 한계가 있다.
@Slf4j
public class ProxyCastingTest {
@Test
void jdkProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(false); //JDK 동적 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
//JDK 동적 프록시를 구현 클래스로 캐스팅 시도 실패, ClassCastException 예외 발생
assertThrows(ClassCastException.class, () -> {
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
});
}
@Test
void cglibProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); //CGLIB 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
log.info("proxy class={}", memberServiceProxy.getClass());
//CGLIB 프록시를 구현 클래스로 캐스팅 시도 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
}
- ProxyFactory의 proxyTargetClass 옵션에 따라 둘 중 하나를 선택해서 프록시를 만들 수 있다
- false인 경우엔 JDK 동적 프록시, true인 경우엔 CGLIB를 사용한다
- 옵션과 무관하게 인터페이스가 없는 경우엔 CGLIB를 사용한다
위 코드에서 JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅이 불가능한 반면, CGLIB 프록시는 대상 객체인 MemberServiceImpl로 캐스팅이 가능하다. 이는 JDK 동적 프록시를 사용할 때 의존관계 주입에서 발생하는 문제점과 강하게 연관되어 있다.


2) 의존관계 주입
JDK 동적 프록시는 인터페이스 타입에 대해서만 의존관계를 주입받을 수 있고, 구체 클래스 타입에 대해서는 의존관계를 주입받을 수 없게 된다. 구체 클래스 타입을 주입 받으려고 하면 UnsatisfiedDependencyException이 발생한다.
@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");
}
}
Error creating bean with name 'hello.aop.proxyvs.ProxyDITest': Unsatisfied dependency expressed through field 'memberServiceImpl'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to be
of type 'hello.aop.member.MemberServiceImpl' but was actually of type 'com.sun.proxy.$Proxy54'
CGLIB 프록시는 인터페이스 타입과 구체클래스 타입 모두에 대해서 의존관계를 주입받을 수 있다.
@SpringBootTest //기본적으로 CGLIB 프록시 사용
//@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");
}
}
3. CGLIB 프록시의 한계
CGLIB를 사용해 프록시를 생성하면 JDK 동적 프록시에서 발생한 문제가 해결되므로 CGLIB가 만능으로 보일 수 있다. 그러나 CGLIB 또한 몇가지 문제점을 가지고 있다.
1) 대상 클래스에 기본 생성자 필수
CGLIB 프록시의 생성자는 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스에 기본 생성자를 정의해줘야 한다.
2) 생성자 2번 호출 문제
자바에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 부모 클래스의 생성자도 호출된다(참고). CGLIB 프록시는 구체 클래스를 상속받으므로 실제 대상 클래스의 객체를 생성할 때, 프록시 객체를 생성하면서 부모 클래스의 생성자를 호출할 때를 합해 총 2번 생성자를 호출한다.

3) final 키워드 클래스, 메서드 사용 불가
final 키워드가 클래스에 붙으면 상속이 불가능하며, 메서드에 붙으면 오버라이딩이 불가능하다. 스프링 AOP에서 프록시는 기본적으로 상속/오버라이딩을 기반으로 구현/동작하므로 final 키워드가 붙은 클래스나 메서드에 대해서는 프록시가 정상적으로 적용되지 않는다. 그러나 일반적인 웹 앱 개발에서는 클래스나 메서드에 final 키워드를 붙여서 사용할 일이 거의 없기 때문에 특별히 문제가 되진 않는다(물론 코틀린 같이 기본적으로 final이 붙은 경우는 예외다).
4. 스프링의 해결책
스프링은 AOP 프록시 생성을 편리하기 위해 앞선 문제들에 대한 해결책들을 제시해왔다.
1) CGLIB를 스프링 내부에 함께 패키징
과거엔 CGLIB를 사용하기 위해선 별도의 CGLIB 라이브가 필요했지만, 스프링 3.2부터는 CGLIB 라이브러리가 스프링 내부에 함께 패키징되어 별도의 라이브러리 추가 없이 CGLIB를 사용할 수 있게 되었다.
2) CGLIB 기본 생성자 필수 문제 해결
스프링 4.0부터는 objenesis라는 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능해졌다. 해당 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.
3) 생성자 2번 호출 문제
스프링 4.0부터 CGLIB의 생성자 2번 호출 문제가 해결되었다. 이것 또한 objenesis 라이브러리 덕분이다.
4) CGLIB 기본 사용
스프링 부트 2.0 부터는 CGLIB를 기본으로 사용하고 있다. 따라서 인터페이스가 존재하더라도 JDK 동적 프록시가 아닌 CGLIB로써 구체클래스를 기반으로 프록시를 생성한다. 이로써 구체 클래스 타입으로 의존관계를 주입받을 시 발생하는 문제가 해결되었다. 물론 원한다면 옵션을 주어 JDK 동적 프록시로 프록시를 생성할 수도 있다.
// application.properties
spring.aop.proxy-target-class=false
정리
스프링은 CGLIB를 기본적으로 사용할 수 있게 지원함으로써 JDK 동적 프록시의 문제인 구체 클래스 주입이 가능하게 만들었다. 또한, 여러 라이브러리를 도입함으로써 CGLIB의 단점들을 많이 개선해왔다. final 클래스나 final 메서드는 AOP 적용 대상에 잘 사용하지 않으므로 크게 문제되지 않는다.
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 포인트컷 (0) | 2023.03.16 |
---|---|
[스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2023.03.07 |
[스프링 핵심 원리 - 고급편] 빈 후처리기 (0) | 2023.03.06 |
본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
1. 프록시의 내부 호출
스프링의 프록시 방식의 AOP는 메서드 내부 호출(self-invocation)에 프록시를 적용할 수 없다는 한계가 있다. 프록시 방식의 AOP는 프록시를 통해서 대상 객체(target)를 호출해야만 AOP가 적용된다. 프록시에서 먼저 어드바이스를 호출한 후 대상 객체를 호출하는 것이다. 그러나 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하게 되므로 AOP가 적용되지 않고 어드바이스도 호출하지 않는다. 내부 호출이 발생하는 예제를 살펴보자.
CallServiceV0 - 내부 호출이 발생
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); //내부 메서드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
CallLogAspect - AOP를 적용하기 위한 Aspect
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* hello.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
CallServiceV0Test 예시
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV0Test {
@Autowired CallServiceV0 callServiceV0;
@Test
void external() {
callServiceV0.external();
}
@Test
void internal() {
callServiceV0.internal();
}
}
external() 테스트 메서드 실행 결과

- external()에는 AOP가 적용됐지만, internal()에는 AOP가 적용되지 않았다.
internal() 테스트 메서드 실행 결과

- internal()에 AOP가 적용되었다.
위와 같은 결과가 나온 까닭은 자바의 특성과 관련있다. internal()과 같이 자바에서 메서드 앞에 별도의 참조가 없으면 자기 자신의 인스턴스(this)의 메서드를 호출한다. 즉 this.internal()이 되는데, 여기서 this는 실제 대상 객체(target)이므로 이러한 형태의 내부 호출은 프록시를 거치지 않은 호출이 된다. 따라서 어드바이스가 호출되지 않고 AOP가 적용되지 않는 것이다.

2. 프록시 내부 호출 대안
**스프링 2.6부터는 자기 자신/세터 주입을 막아놓았다고 한다...
1) 대안1 - 자기 자신 주입
가장 단순한 해결 방법은 자기 자신을 주입받는 것이다. 생성자 주입으로는 자기 자신을 주입받을 수 없으므로 여기서는 수정자 주입을 사용했다.
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal(); //외부 메서드 호출
}
public void internal() {
log.info("call internal");
}
}
- 순환 참조 문제가 발생하므로 생성자 주입은 안 되고, 대신 setter로 의존성을 주입받는다 (수정자 주입)
자기 자신을 주입받으면 내부 호출이 일어나더라도, 호출 대상 또한 프록시이므로 AOP가 잘 적용된다. 다만 일반적인 생성자 주입으로는 순환 참조 문제로 인해 애플리케이션 실행이 되지 않는다는 점을 주의하자.

2) 대안2 - 지연 조회
자기 자신을 주입받는 대신 지연 조회를 사용하는 방법도 있다. 스프링 빈을 지연 조회하기 위해선 ApplicationContext나 ObjectProvider을 사용하면 된다.
@Slf4j
@Component
public class CallServiceV2 {
private final ObjectProvider<CallServiceV2> callServiceProvider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
log.info("call external");
CallServiceV2 callServiceV2 = callServiceProvider.getObject();
callServiceV2.internal(); //외부 메서드 호출
}
public void internal() {
log.info("call internal");
}
}
- ObjectProvider를 사용하여 스프링 컨테이너에서 빈을 조회한다
- 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점에 객체를 스프링 컨테이너에서 조회할 수 있다 (지연조회)
3) 대안3 - 구조 변경
앞선 해결방법1과 해결방법2는 자기 자신이나 Provider를 주입받아야 한다는 점에서 다소 어색한 감이 있다. 내부 호출 자체가 발생하지 않도록 구조를 변경하는 것이 가장 권장되는 해결방법이다.
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {
private final InternalService internalService;
public void external() {
log.info("call external");
internalService.internal(); //외부 메서드 호출
}
}
@Slf4j
@Component
public class InternalService {
public void internal() {
log.info("call internal");
}
}
- 내부 호출할 메서드를 별도의 객체로 옮기고, 해당 객체를 주입받는 식으로 구조가 변경되었다.
별도의 서비스로 분리하는 방법 말고도 클라이언트에서 external(), internal() 메서드를 각각 호출하는 방식으로도 구조를 변경할 수 있다(external() 메서드가 internal() 메서드를 호출하지 않는다는 가정하에). 또한, AOP는 public 메서드에만 적용되므로 내부 호출 문제는 public 메서드에서 public 메서드를 내부 호출하는 경우에만 발생한다.

2. JDK 동적 프록시의 한계
1) 타입 캐스팅
인터페이스가 없고 구체 클래스만 있는 경우엔 CGLIB를 사용해서 프록시를 생성해야 하지만, 인터페이스가 있는 경우엔 JDK 동적 프록시와 CGLIB 둘 중에서 하나를 선택할 수 있다. 인터페이스를 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능하다는 한계가 있다.
@Slf4j
public class ProxyCastingTest {
@Test
void jdkProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(false); //JDK 동적 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
//JDK 동적 프록시를 구현 클래스로 캐스팅 시도 실패, ClassCastException 예외 발생
assertThrows(ClassCastException.class, () -> {
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
});
}
@Test
void cglibProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); //CGLIB 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
log.info("proxy class={}", memberServiceProxy.getClass());
//CGLIB 프록시를 구현 클래스로 캐스팅 시도 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
}
- ProxyFactory의 proxyTargetClass 옵션에 따라 둘 중 하나를 선택해서 프록시를 만들 수 있다
- false인 경우엔 JDK 동적 프록시, true인 경우엔 CGLIB를 사용한다
- 옵션과 무관하게 인터페이스가 없는 경우엔 CGLIB를 사용한다
위 코드에서 JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅이 불가능한 반면, CGLIB 프록시는 대상 객체인 MemberServiceImpl로 캐스팅이 가능하다. 이는 JDK 동적 프록시를 사용할 때 의존관계 주입에서 발생하는 문제점과 강하게 연관되어 있다.


2) 의존관계 주입
JDK 동적 프록시는 인터페이스 타입에 대해서만 의존관계를 주입받을 수 있고, 구체 클래스 타입에 대해서는 의존관계를 주입받을 수 없게 된다. 구체 클래스 타입을 주입 받으려고 하면 UnsatisfiedDependencyException이 발생한다.
@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");
}
}
Error creating bean with name 'hello.aop.proxyvs.ProxyDITest': Unsatisfied dependency expressed through field 'memberServiceImpl'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to be
of type 'hello.aop.member.MemberServiceImpl' but was actually of type 'com.sun.proxy.$Proxy54'
CGLIB 프록시는 인터페이스 타입과 구체클래스 타입 모두에 대해서 의존관계를 주입받을 수 있다.
@SpringBootTest //기본적으로 CGLIB 프록시 사용
//@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");
}
}
3. CGLIB 프록시의 한계
CGLIB를 사용해 프록시를 생성하면 JDK 동적 프록시에서 발생한 문제가 해결되므로 CGLIB가 만능으로 보일 수 있다. 그러나 CGLIB 또한 몇가지 문제점을 가지고 있다.
1) 대상 클래스에 기본 생성자 필수
CGLIB 프록시의 생성자는 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스에 기본 생성자를 정의해줘야 한다.
2) 생성자 2번 호출 문제
자바에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 부모 클래스의 생성자도 호출된다(참고). CGLIB 프록시는 구체 클래스를 상속받으므로 실제 대상 클래스의 객체를 생성할 때, 프록시 객체를 생성하면서 부모 클래스의 생성자를 호출할 때를 합해 총 2번 생성자를 호출한다.

3) final 키워드 클래스, 메서드 사용 불가
final 키워드가 클래스에 붙으면 상속이 불가능하며, 메서드에 붙으면 오버라이딩이 불가능하다. 스프링 AOP에서 프록시는 기본적으로 상속/오버라이딩을 기반으로 구현/동작하므로 final 키워드가 붙은 클래스나 메서드에 대해서는 프록시가 정상적으로 적용되지 않는다. 그러나 일반적인 웹 앱 개발에서는 클래스나 메서드에 final 키워드를 붙여서 사용할 일이 거의 없기 때문에 특별히 문제가 되진 않는다(물론 코틀린 같이 기본적으로 final이 붙은 경우는 예외다).
4. 스프링의 해결책
스프링은 AOP 프록시 생성을 편리하기 위해 앞선 문제들에 대한 해결책들을 제시해왔다.
1) CGLIB를 스프링 내부에 함께 패키징
과거엔 CGLIB를 사용하기 위해선 별도의 CGLIB 라이브가 필요했지만, 스프링 3.2부터는 CGLIB 라이브러리가 스프링 내부에 함께 패키징되어 별도의 라이브러리 추가 없이 CGLIB를 사용할 수 있게 되었다.
2) CGLIB 기본 생성자 필수 문제 해결
스프링 4.0부터는 objenesis라는 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능해졌다. 해당 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.
3) 생성자 2번 호출 문제
스프링 4.0부터 CGLIB의 생성자 2번 호출 문제가 해결되었다. 이것 또한 objenesis 라이브러리 덕분이다.
4) CGLIB 기본 사용
스프링 부트 2.0 부터는 CGLIB를 기본으로 사용하고 있다. 따라서 인터페이스가 존재하더라도 JDK 동적 프록시가 아닌 CGLIB로써 구체클래스를 기반으로 프록시를 생성한다. 이로써 구체 클래스 타입으로 의존관계를 주입받을 시 발생하는 문제가 해결되었다. 물론 원한다면 옵션을 주어 JDK 동적 프록시로 프록시를 생성할 수도 있다.
// application.properties
spring.aop.proxy-target-class=false
정리
스프링은 CGLIB를 기본적으로 사용할 수 있게 지원함으로써 JDK 동적 프록시의 문제인 구체 클래스 주입이 가능하게 만들었다. 또한, 여러 라이브러리를 도입함으로써 CGLIB의 단점들을 많이 개선해왔다. final 클래스나 final 메서드는 AOP 적용 대상에 잘 사용하지 않으므로 크게 문제되지 않는다.
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 포인트컷 (0) | 2023.03.16 |
---|---|
[스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2023.03.07 |
[스프링 핵심 원리 - 고급편] 빈 후처리기 (0) | 2023.03.06 |