일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch13
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch11
- 스프링 mvc1 - 스프링 mvc
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 타임리프
- 스프링 mvc2 - 검증
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch14
- 스프링 입문(무료)
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch5
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch8
- 스프링 db2 - 데이터 접근 기술
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch4
- 게시글 목록 api
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch7
- @Aspect
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch12
- Today
- Total
나구리의 개발공부기록
스프링 AOP - 실무 주의사항, 프록시와 내부 호출(문제/대안), 프록시 기술과 한계(타입캐스팅/의존관계 주입/CGLIB/스프링의 해결책) 본문
스프링 AOP - 실무 주의사항, 프록시와 내부 호출(문제/대안), 프록시 기술과 한계(타입캐스팅/의존관계 주입/CGLIB/스프링의 해결책)
소소한나구리 2024. 11. 16. 17:25출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프록시와 내부 호출 - 문제
1) 문제점
(1) 문제 설명
- 스프링은 프록시 방식의 AOP를 사용하므로 AOP를 적용하려면 항상 프록시를 통해서 대상 객체(target)을 호출해야 프록시에서 먼저 어드바이스를 호출하고 이후에 대상 객체를 호출함
- 만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고 어드바이스도 호출되지 않음
- AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링빈으로 등록하여 의존관계 주입시에 항상 프록시 객체를 주입함
- 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않음
- 그러나 대상 객체의 내부에서 메서드 호출이 발생하며 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생함
- 해당 문제는 실무에서 반드시 한번은 만나서 고생하는 문제이기때문에 꼭 이해하고 넘어가야함
2) 예제
(1) CallServiceV0
- internalcall 패키지를 생성하여 작성
- 외부에서 external()을 호출하면 내부에서 internal()이라는 자기 자신의 메서드를 호출
- 자바 언어에서 메서드를 호출할 때 대상을 지정하지 않으면 앞에 자기 자신의 인스턴스를 뜻하는 this가 자동으로 붙어서 실행됨
package hello.aop.internalcall;
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); // 내부 메서드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
(2) CallLogAspect
- internalcall 패키지 하위에 aop패키지를 생성하여 작성
- CallServiceV0에 적용할 AOP
package hello.aop.internalcall.aop;
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* hello.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
(3) CallServiceV0Test
- @Import로 CallLogAspect를 스프링빈으로 등록하여 CallServiceV0에 AOP프록시를 적용
- @SpringBootTest: 내부에 컴포넌트 스캔을 포함하고 있어 CallServiceV0에 적용한 @Component를 스프링 빈으로 적용하여 테스트를 수행함
package hello.aop.internalcall;
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV0Test {
@Autowired CallServiceV0 callServiceV0;
@Test
void external() {
callServiceV0.external();
}
@Test
void internal() {
callServiceV0.internal();
}
}
(4) external() 실행 결과 및 동작 설명
- 실행 결과를 보면 callServiceV0.external()을 실행할 때는 프록시를 호출하여 callLogAspect 어드바이스가 호출되고 실제객체의 external()을 호출함
- 그런데 여기에서의 문제는 callServiceV0.external() 내부에서 internal()을 호출할 때 발생하는데, 이때는 CallLogAspect어드바이스가 호출되지 않음
- 자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리키므로 결과적으로 자기 자신의 내부 메서드를 호출하는 this.internal()이 되는데, 여기서 this는 프록시가 아니라 실제 대상 객체(target)의 인스턴스를 뜻함
- 결과적으로 external()내부에서 호출한 internal()은 프록시를 거치지 않기 때문에 어드바이스도 적용할 수 없음
(5) internal() 실행 결과 및 동작 설명
- 외부에서 internal()을 호출하는 테스트의 결과를 보면 이 경우에는 프록시를 거치기 때문에 CallLogAspect 어드바이스가 적용된 모습을 확인할 수 있음
(6) 프록시 방식의 AOP 한계
- 스프링은 프록시 방식의 AOP를 사용하며 프록시 방식의 AOP는 메서드 내부 호출에 프록시를 적용할 수 없음
** 참고
- 실제 코드에 AOP를 직접 적용하는 AspectJ 프레임워크를 사용하면 이런 문제가 발생하지 않음
- 프록시를 통하는 것이 아니라 해당 코드에 직접 AOP 적용 코드가 붙어있기 때문에 내부 호출과 무관하게 AOP를 적용할 수 있음
- 하지만 로드 타임 위빙 등을 사용해야하므로 설정의 복잡함과 JVM 옵션을 주어야하는 부담이 있으며, 이후에 설명할 프록시 방식의 AOP에서 내부 호출의 문제점을 대응할 수 있는 대안들이 있으므로 AspectJ를 직접 사용하는 방법은 실무에서 거의 사용하지 않음
- 스프링 애플리케이션과 함께 직접 AspectJ를 사용하는 방법은 스프링 공식 메뉴얼을 참고
- 공식메뉴얼
2. 대안1 - 자기 자신 주입
** 주의! - 스프링 부트 2.6 이상 사용시 설정 변경필요
- 스프링 부트 2.6 부터는 순환 참조를 기본적으로 금지하도록 정책이 변경되어 생성자 주입이 아닌 수정자 주입을 하여도 순환참조 에러가 발생함
- 해당 문제를 해결하려면 application.properties에 spring.main.allow-circular-references=true를 추가해야함
- 이후의 다른 테스트에도 영향을 주기 때문에 스프링 2.6 이상이라면 해당 설정을 추가하고 실습을 진행해야함
1) 내부호출을 해결하는 가장 간단한 방법
- 자기 자신을 의존관계 주입받는 것
(1) CallServiceV1
- 수정자를 통해서 자기자신을 주입
- 스프링 AOP가 적용된 대상을 의존관계 주입을 받으면 주입 받은 대상은 실제 자신이 아니라 프록시 객체임
- external()을 내부에서 callServiceV1.internal()를 호출하도록 작성하여 주입받은 프록시를 통해 internal()을 호출이 되어 AOP가 적용됨
** 참고
- 이 경우 생성자 주입하면 오류가 발생하는데 본인을 생성하면서 주입해야하기때문에 순환사이클이 만들어짐
- 반면 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 오류가 발생하지 않음 - 위 주의! 확인필요
package hello.aop.internalcall;
/**
* 참고: 자기 자신을 의존관계 주입받을 때 생성자 주입을 사용하면 순환 사이클을 만들기 때문에 실패함
*/
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
// setter 주입
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal(); // 자기 자신을 참조한 변수로 internal()을 외부호출
}
public void internal() {
log.info("call internal");
}
}
(2) CallServiceV1Test 실행결과
- CallServiceV1을 의존관계 주입하여 external()을 테스트 실행해보면 internal()에 AOP가 적용된 것을 확인할 수 있음
@Slf4j
@Import(CallLogAspect.class)
@SpringBootTest
class CallServiceV1Test {
@Autowired CallServiceV1 callServiceV1;
@Test
void external() {
callServiceV1.external();
}
}
3. 대안2 - 지연 조회
1) 스프링 빈을 지연 조회
- ObjectProvider(Provider), ApplicationContext를 사용
(1) CallServiceV2
- ApplicationContext나 ObjectProvider를 사용하면 객체를 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연시킬 수 있음
- ApplicationContext는 너무 많은 기능을 제공하므로 직접 ApplicationContext를 사용하는 것보다 기본편에서 학습한 ObjectProvider를 활용하는 것이 더 나음
- callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회함
- 자기 자신을 주입 받는 것이 아니기 때문에 순환 사이클이 발생하지 않는다
package hello.aop.internalcall;
@Slf4j
@Component
public class CallServiceV2 {
// ApplicationContext 활용, 기능이 너무 많기 때문에 권장하지 않음
// private final ApplicationContext applicationContext;
// public CallServiceV2(ApplicationContext applicationContext) {
// this.applicationContext = applicationContext;
// }
// ObjectProvider 활용
private final ObjectProvider<CallServiceV2> callServiceProvider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
log.info("call external");
// 스프링 빈을 지연 조회
// CallServiceV2 callServiceV2 = applicationContext.getBean(CallServiceV2.class);
CallServiceV2 callServiceV2 = callServiceProvider.getObject();
callServiceV2.internal();
}
public void internal() {
log.info("call internal");
}
}
(2) CallserviceV2Test 작성 및 실행
- CallServiceV2를 의존관계 주입 받아서 위의 테스트케이스와 동일하게 external()을 호출하는 테스트를 실행하면 external()메서드 내부에서 호출되는 internal()메서드에도 AOP가 적용되는 모습을 확인할 수 있음
4. 대안3 - 구조 변경
1) 가장 나은 대안
- 앞선 방법들은 자기 자신을 주입하거나 Provider를 사용하여 조금 억지로 문제를 해결하였음(어색한 해결 방법)
- 가장 나은 대안으로는 내부 호출이 발생하지 않도록 구조를 변경하는 것이며 스프링에서도 이 방법을 가장 권장함
(1) CallServiceV3
- CallService에 있던 internal() 메서드를 InternalService라는 별도의 클래스로 분리하고 해당 클래스를 의존관계를 주입받아서 외부에서 internal()메서드를 호출
- 즉, external()메서드가 호출되면 internalService의 internal()메서드가 호출되어 내부호출 구조자체가 분리 되었음
package hello.aop.internalcall;
/**
* 구조를 변경(분리)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CallServiceV3 {
private final InternalService internalService;
public void external() {
log.info("call external");
internalService.internal();
}
}
(2) InternalService
- internal()을 가지고 있는 InternalService클래스
@Slf4j
@Component
public class InternalService {
public void internal() {
log.info("call internal");
}
}
(3) CallServiceV3Test 작성 및 실행
- 테스트클래스에서 의존관계를 CallServiceV3로 변경하고 external()메서드를 호출하는 테스트 케이스를 실행하면 정상적으로 AOP가 모두 적용되는 로그를 확인할 수 있음
- 내부 호출 자체가 사라지고 callService -> internalService를 호출하는 구조로 변경되어 자연스럽게 AOP가 적용됨
- 여기서 구조를 변경한다는 것은 단순하게 분리하는 것뿐만아니라 클라이언트에서 두 메서드를 호출하는 등의 다양한 방법을 적용하여 문제를 해결할 수 있음
- 물론 클라이언트에서 두 메서드를 호출하여 문제를 해결할 때에는 external()에서 internal()을 내부 호출하지 않도록 코드를 변경하야 가능하므로 비즈니스와 현재 애플리케이션의 구조를 보고 적절하게 적용하면 됨
** 참고
- AOP는 주로 트랜잭션 적용이나 주요 컴포넌트의 로그 출력 기능에 사용됨
- 즉, 인터페이스에 메서드가 나올 정도의 규모에 AOP를 적용하는 것이 적당하며 public 메서드에만 적용함
- 애초에 private 메서드처럼 작은단위에는 AOP를 적용하지도 않고 할 수도 없으며 AOP 적용을 위해 private 메서드를 외부 클래스로 변경하고 public으로 변경하는 일은 거의 없음
- public 메서드에서 public 메서드를 내부 호출 하는 경우에 이런 문제가 발생하며 실무에서 꼭 한번은 만나는 문제이기 때문에 AOP가 잘 적용되지 않으면 내부 호출을 의심해봐야 함
5. 프록시 기술과 한계 - 타입 캐스팅
1) JDK 동적 프록시 CGLIB 프록시
(1) 리마인드
- JDK 동적 프록시는 인터페이스가 필수이고 인터페이스를 기반으로 프록시를 생성
- CGLIB는 구체 클래스를 기반으로 프록시를 생성
- 인터페이스가 없고 구체클래스만 있는 경우에는 CGLIB를 사용해야만 하며 인터페이스가 있는 경우에는 둘중에 하나를 선택할 수 있음
- 스프링이 프록시를 만들 때 제공하는 ProxyFactory에 proxyTargetClass 옵션에 따라서 둘중 하나를 선택해서 만들 수 있음
- proxyTargetClass=false로 하면 JDK 동적 프록시를 사용하고 true로 하면 CGLIB를 사용하여 프록시를 생성하며 기본값은 false임
- 옵션과 무관하게 인터페이스가 없으면 JDK 동적 프록시를 적용할 수 없으므로 CGLIB를 사용함
(2) JDK 동적 프록시 한계
- 인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있음
2) 예제
(1) ProxyCastingTest
- test하위에 proxyvs 패키지를 생성 후 작성
- JDK 동적 프록시를 사용하도록 설정하여 ProxyFactory로 MemberServiceImpl을 target으로하여 프록시를 생성
- proxyFactory.getProxy()로 프록시를 조회할 때 MemberService 인터페이스 타입으로 캐스팅하면 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 당연히 성공함
- 그러나, 다시 MemberServiceImpl 구체클래스 타입으로 형변환을 하고자하면 형변환 에러가 발생함
package hello.aop.proxyvs;
@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();
// memberServiceProxy -> MemberServiceImpl 타입캐스팅 - 실패 (ClassCastException 에러발생)
assertThatThrownBy(() -> {
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}).isInstanceOf(ClassCastException.class);
}
}
(2) jdkProxy() 테스트 설명
- MemberServiceImpl 타입을 기반으로 JDK 동적 프록시를 생성하도록 코드를 작성하였지만 MemberServiceImpl은 MemberService인터페이스를 구현하므로 JDK 동적 프록시는 MemberService 인터페이스를 기반으로 프록시를 생성함
- JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 이렇게 생성된 인터페이스 기반의 프록시는 MemberService 인터페이스타입으로는 캐스팅이 가능함
- 그러나 구현체인 MemberServiceImpl을 전혀 알지 못하기 때문에 구체 클래스 타입으로 형변환을 시도하면 ClassCastException.class(형변환 예외)가 발생함
(3) ProxyCastingTest - cglibProxy()추가
- CGLIB를 사용하는 테스트 케이스 추가
- setProxyTargetClass(true)를 적용하여 CGLIB를 사용하여 프록시를 사용하면 구체클래스로의 형변환이 가능하여 테스트가 통과됨
@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());
// 다시 구체 클래스로 형변환 - 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
(4) cglibProxy() 테스트 설명
- MemberServiceImpl 타입을 기반으로 CGLIB 프록시를 생성
- CGLIB는 구체 클래스를 기반으로 프록시를 생성하였기 때문에 생성된 프록시는 MemberServiceImpl은 물론이고, MemberServiceImpl이 구현한 인터페이스인 MemberService로도 캐스팅할 수 있음
(5) 정리
- JDK 동적 프록시는 대상 객체인 MemberServiceImpl로 캐스팅 할 수 없지만 CGLIB 프록시는 캐스팅 할 수 있음
6. 프록시 기술과 한계 - 의존관계 주입
1) JDK 동적 프록시의 의존관계 주입 문제
(1) ProxyDIAspect
- proxyvs패키지 하위에 code패키지를 만든 후 작성
- hello.aop와 그 하위의 모든 대상에 간단히 로그를 출력하는 애스펙트
package hello.aop.proxyvs.code;
@Slf4j
@Aspect
public class ProxyDIAspect {
@Before("execution(* hello.aop..*.*(..))")
public void doTrace(JoinPoint joinPoint) {
log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
}
}
(2) ProxyDITest
- application.properties에 설정을 적용하는 대신 @SpringBootTest의 애노테이션으로 옵션을 적용하여 해당 테스트에만 임시로 스프링이 AOP를 생성할 때 JDK 동적 프록시를 우선 생성하도록 설정, 물론 인터페이스가 없으면 CGLIB를 사용함
- @Import로 ProxyDIAspect를 스프링 빈으로 등록
package hello.aop.proxyvs;
@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");
}
}
(3) 테스트 실패
- 테스트코드를 실행해보면 에러메세지와 함께 테스트를 실패하는데, memberServiceImpl에 주입되길 기대하는 타입이 hello.aop.member.MemberServiceImpl이지만 실제 넘어온 타입이 com.sun.proxy.$Proxy57이여서 타입 예외가 발생하였음
- JDK 동적 프록시로 생성된 프록시는 인터페이스를 기반으로 만들어졌기 때문에 MemberService 인터페이스의 의존관계 주입이 가능하지만 MemberServiceImpl타입은 전혀 알 수 없기 때문에 의존관계 주입도 할 수 없음
(4) CGLIB사용 - 테스트 통과
//@SpringBootTest(properties = {"spring.aop.proxy-target-class=false"}) // JDK 동적 프록시 사용 - 테스트 실패(에러발생)
@SpringBootTest(properties = {"spring.aop.proxy-target-class=true"}) // CGLIB 사용 - 성공
- 설정을 CGLIB를 사용하도록 변경한 후 테스트를 실행하면 테스트 통과와 함께, CGLIB 프록시로 AOP가 적용되는 로그를 확인할 수 있음
- CGLIB로 생성된 프록시는 구체 클래스를 상속받아서 생성되기 때문에 구체클래스는 물론 구현한 인터페이스까지도 모두 의존관계 주입을 할 수 있음
(5) 정리
- JDK 동적 프록시는 대상 객체인 MemberServiceImpl 타입에 의존관계를 주입할 수 없고 CGLIB프록시는 의존관계 주입이 가능함
- JDK 동적 프록시가 가지는 한계점을 알아 보았는데, 실제로 개발할 때는 인터페이스가 있으면 인터페이스를 기반으로 의존관계 주입을 받는 것이 맞음
- MemberServiceImpl 타입으로 의존관계를 주입받는 것처럼 구현 클래스에 의존관계를 주입하면 향후 구현 클래스를 변경할 때 클라이언트의 코드도 함께 변경해야하는 문제가 있음
- DI의 장점이 바로 의존관계 주입을 받는 클라이언트 코드의 변경 없이 구현 클래스를 변경할 수 있는 것인데, 이렇게 하려면 인터페이스를 기반으로 의존관계를 주입받아야하므로 올바르게 잘 설계된 애플리케이션이라면 이런 문제가 자주 발생하지는 않음
- 그럼에도 불구하고 테스트 또는 여러가지 이유로 AOP 프록시가 적용된 구체 클래스를 직접 의존관계 주입을 받아야하는 경우가 있을 수 있는데 이때는 CGLIB를 통해 구체 클래스 기반으로 AOP 프록시를 적용하면 됨
- 사실 CGLIB를 사용하면 이런 고민 자체를 하지 않아도 되지만 CGLIB도 단점이 존재함
7. 프록시 기술과 한계 - CGLIB
1) CGLIB 구체 클래스 기반 프록시 문제점
(1) 대상 클래스에 기본 생성자 필수
- 자바 언어에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 함
- 이 부분이 생략되어 있다면 자식 클래스의 생성자 첫줄에 부모 클래스의 기본 생성자를 호출하는 super()가 자동으로 들어가며 이러한 동작방식은 자바 문법 규약임
- CGLIB를 사용할 때 CGLIB가 만드는 프록시의 생성하는 개발자가 직접 호출하지 않고 CGLIB프록시가 대상 클래스를 상속받고 생성자에서 대상 클래스의 기본생성자를 호출하기 때문에 대상 클래스에 기본 생성자를 만들어 주어야 함
- 자바 문법상 생성자가 하나도 없으면 자동으로 만들어지며 파라미터가 있는 생성자가 있다면 직접 기본생성자를 만들어 주어야 함
(2) 생성자 2번 호출 문제
- CGLIB 프록시를 사용하면 실제 target의 객체를 생성할 때 생성자를 1번 호출하고, 프록시 객체를 생성할 때 부모 클래스의 생성자를 1번 더 호출하여 총 2번 호출됨
(3) final 키워드 클래스, 메서드 사용 불가
- final 키워드가 클래스에 있으면 상속이 불가능하고 메서드에 있으면 오버라이딩이 불가능하기 때문에 상속을 기반으로 하는 CGLIB가 프록시를 생성되지 않거나 정상 동작하지 않음
- 프레임워크 같은 개발이 아니라 일반적인 웹 애플리케이션을 개발할 때는 final 키워드를 잘 사용하지 않기 때문에 해당 문제는 특별히 문제가 되지는 않음
(4) 정리
- JDK 동적 프록시는 대상 클래스 타입으로 주입할 때 문제가 있음
- CGLIB는 대상 클래스에 기본 생성자가 필수이고 생성자가 2번 호출되는 문제가 있음
8. 프록시 기술과 한계 - 스프링의 해결책
1) 스프링의 기술 선택 변화
(1) 스프링 3.2, CGLIB를 스프링 내부에 함께 패키징
- CGLIB를 사용하려면 CGLIB 라이브러리가 별도로 필요했었으나 스프링 3.2 부터는 스프링 내부에 함께 패키징하여 별도의 라이브러리 추가 없이 CGLIB를 사용할 수 있게 되었음
- CGLIB spring-core org.springframework
(2) objenesis 라이브러리 사용으로 문제 해결
- 스프링 4.0 부터 objenesis라는 특별한 라이브러리를 사용하여 기본 생성자 없이 객체 생성이 가능하게 되었음
- 해당 라이브러리가 생성자 호출 없이 객체를 생성할 수 있게 해줌으로써 프록시를 생성할 때 부모클래스의 생성자를 호출할 필요가 없게됨
- 즉, 기본 생성자가 필수인 문제와 생성자가 2번 호출되는 문제가 모두 한방에 해결되었음
(3) 스프링 부트 2.0 - CGLIB 기본사용
- 스프링 부트 2.0 버전부터 CGLIB를 기본으로 사용하도록 하였기 때문에 스프링 부트로 프로젝트를 생성하고 별도의 설정을 하지 않는다면 AOP를 적용할 때 기본적으로 proxyTargetClass=true 설정이 적용되어 인터페이스가 있어도 항상 CGLIB를 사용하여 구체클래스 기반으로 프록시를 생성함
- 물론 스프링은 우리에게 선택권을 열어주기 때문에 여러 예제를 통해 설정을 적용했던 것처럼 JDK 동적 프록시도 사용할 수 있음
(4) 정리
- 스프링은 최종적으로 CGLIB를 기본으로 사용하도록 결정하여 JDK 동적 프록시에서 동작하지 않는 구체 클래스 주입이 가능하도록 문제를 해결하였으며 추가적인 CGLIB의 단점들도 해결하였음
- CGLIB의 남은 문제라면 final클래스나 메서드가 있는데, AOP를 적용할 대상에는 final 클래스나 메서드를 잘 사용하지 않기 때문에 이부분은 크게 문제가 되지 않음
- 개발자 입장에서보면 어떤 프록시 기술을 사용하든 크게 상관이 없으며 심지어 새로운 프록시 기술을 사용해도 문제없고 개발하기에 편리하면 됨
- 심지어 클라이언트 입장에서는 어떤 프록시 기술을 사용하는지 모르고 잘 동작하는 것이 가장 좋음