스프링 AOP 실무 주의사항
프록시와 내부 호출 - 문제
- 스프링은 프록시 방식의 AOP를 사용한다.
- AOP 적용 시 스프링은 실제 객체 대신 프록시를 스프링 빈으로 등록한다.
- 스프링은 의존관계 주입 시 항상 프록시 객체를 주입한다.
- AOP를 적용하기 위해서 항상 프록시를 통해서 실제 객체(Target)을 호출해야 한다.
- 프록시에서 어드바이스를 호출하고 이후 실제 객체를 호출한다.
- 프록시를 거치지않고 실제 객체를 직접 호출하면 AOP가 적용되지 않는다.
- 실제 객체 내부에서 메소드 호출이 발생하면 프록시를 거치지 않고 실제 객체를 직접 호출하는 문제가 발생한다.
- AOP 적용 시 스프링은 실제 객체 대신 프록시를 스프링 빈으로 등록한다.
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); //내부 메소드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
- 내부 호출 예제
- external()을 호출하면 내부에서 internal()이라는 자신의 메소드를 호출한다.
- 자바에서 메소드 호출 시 대상을 지정하지 않으면 자기 자신의 인스턴스를 의미하는 this가 붙게된다.
- ex) internal() == this.internal()
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* hello.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
- Aspect 예제
@Import(CallLogAspect.class)
@SpringBootTest
@Slf4j
class CallServiceV0Test {
@Autowired
CallServiceV0 callServiceV0;
@Test
void external() {
callServiceV0.external();
}
@Test
void internal() {
callServiceV0.internal();
}
}
- 내부 호출 테스트 예제
- external()
- 실행 과정 : callServiceV0.external() 호출 -> 프록시 호출 -> 어드바이스 적용 -> 실제 callServiceV0 객체의 external() 호출 -> this.internal() 호출
- callServiceV0.external()은 프록시에 의해 호출되기에 AOP가 적용되지만 internal()은 프록시가 아닌 내부에서 메소드를 호출했기에 AOP가 적용되지 않는다.
- internal()
- 실행 과정 : callServiceV0.internal() 호출 -> 프록시 호출 -> 어드바이스 적용 -> 실제 callServiceV0 객체의 internal() 호출
- 여기서 callServiceV0.internal()은 프록시에 의해 호출되기에 AOP가 적용된다.
- external()
- 프록시 방식의 AOP 한계점
- 프록시 방식의 AOP는 메소드 내부 호출에 프록시를 적용할 수 없다.
- 직접 AOP를 적용하는 AspectJ 방식을 사용하면 내부 호출에도 프록시를 적용할 수 있으나 복잡하기에 실무에서 잘 사용되지 않는다.
- 프록시 방식의 AOP는 메소드 내부 호출에 프록시를 적용할 수 없다.
프록시와 내부 호출 대안1 - 자기 자신 주입
- 내부 호출을 해결하는 가장 간단한 방법은 자기 자신을 의존관계 주입받는 것이다.
/**
* 생성자 주입을 사용하면 순환 사이클이 만들어지기에 실패한다.
*/
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
log.info("callServiceV1 setter={}", callServiceV1.getClass());
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal(); //외부 메소드 호출
}
public void internal() {
log.info("call internal");
}
}
- 내부 호출 문제를 해결하기 위한 예제V1
- @Autowired CallServiceV1 callServiceV1으로 의존관계 주입
- 의존관계 주입을 받는 callSeviceV1은 실제 객체가 아닌 스프링 빈으로 등록된 프록시 객체
- 내부 호출 문제가 발생하는 이유는 자기 자신의 인스턴스의 메소드인 this.internal()을 호출하기 때문
- this.internal()이 아닌 내부에서 의존관계 주입을 통해 자기 자신의 객체가 아닌 프록시 객체를 주입받고 프록시를 통해 호출하면 문제를 해결할 수 있다.
- 프록시 객체를 의존관계 주입받을 때 생성자 주입 대신 수정자(setter) 주입을 사용하는 이유는 생성자 주입을 하면 자기 자신을 생성하면서 주입해야하기 때문에 순환 사이클이 만들어지고 오류가 발생한다.
- 스프링 부트 2.6부터는 순환 참조가 금지되었기에 수정자 주입을 사용해도 오류가 발생하기에 application.properties에 spring.main.allow-circular-references=true 속성을 추가해야 한다.
- @Autowired CallServiceV1 callServiceV1으로 의존관계 주입
프록시와 내부 호출 대안2 - 지연 조회
- 스프링 빈을 지연해서 조회해도 내부 호출 문제를 해결할 수 있다.
- ObjectProvider(Provider) / ApplicationContext를 사용하면 된다.
@Slf4j
@Component
public class CallServiceV2 {
//ApplicationContext는 스프링에서 주입받을 수 있도록 제공해준다.
private final ApplicationContext applicationContext;
public CallServiceV2(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void external() {
log.info("call external");
CallServiceV2 callServiceV2 = applicationContext.getBean(CallServiceV2.class);
callServiceV2.internal(); //외부 메소드 호출
}
public void internal() {
log.info("call internal");
}
}
- ApplicationContext를 사용한 지연 조회
- 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를 사용한 지연 조회
- ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 시점을 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점으로 지연할 수 있다.
- callServiceProvider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
- 자기 자신을 주입받는 것이 아니기에 순환 사이클이 발생하지 않는다.
- ObjectProvider는 객체를 스프링 컨테이너에서 조회하는 시점을 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점으로 지연할 수 있다.
프록시와 내부 호출 대안3 - 구조 변경
- 내부 호출이 발생하지 않도록 구조 자체를 변경하는 방법
/**
* 구조를 변경(분리)
*/
@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");
}
}
- 구조를 변경함으로 내부 호출 문제를 해결한 예제
- 기존에는 CallService에 external()과 internal()이 함께 있었고 external() 내부에서 internal()을 호출하면서 문제가 발생했다.
- internal() 메소드를 별도의 InternalService 클래스로 분리함으로 내부 호출이 아닌 CallService -> InternalService를 호출하는 외부 호출이 되도록 구조 자체를 변경
- 기존에는 CallService에 external()과 internal()이 함께 있었고 external() 내부에서 internal()을 호출하면서 문제가 발생했다.
프록시 기술과 한계 - 타입 캐스팅
- JDK 동적 프록시와 CGLIB를 사용해서 AOP 프록시를 만드는 방법에는 장단점이 존재한다.
- JDK 동적 프록시는 인터페이스가 필수이며 인터페이스를 기반으로 프록시를 생성한다.
- CGLIB는 구체 클래스를 기반으로 프록시를 생성한다.
- 인터페이스가 없고 구체 클래스만 있으면 CGLIB를 사용해야하며 인터페이스가 있는 경우에는 JDK 동적 프록시나 CGLIB 중 하나를 선택할 수 있다.
- 스프링이 프록시를 만들 때 제공하는 ProxyFactory의 proxyTargetClass 옵션에 따라 둘 중 하나를 선택해서 프록시를 만들 수 있다.
- proxyTargetClass=false : JDK 동적 프록시를 사용해서 인터페이스 기반 프록시 생성
- proxyTargetClass=true : CGLIB를 사용해서 구체 클래스 기반 프록시 생성
- 옵션과 무관하게 인터페이스가 없으면 CGLIB를 사용한다.
- 인터페이스 기반으로 프록시를 생성하는 JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능하다는 한계가 있다.
@Test
public 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;
});
}
- JDK 동적 프록시를 사용해 프록시를 생성하고 타입 캐스팅하는 예제
- MemberServiceImpl 타입을 기반으로 JDK 동적 프록시 생성
- proxyFactory.setProxyTargetClass(false)로 JDK 동적 프록시 생성
- MemberServiceImpl 타입은 MemberService 인터페이스를 구현했기에 MemberService 인터페이스를 기반으로 프록시를 생성한다.
- 프록시는 MemberService 인터페이스를 구현해서 생성했기에 MemberService로 타입 캐스팅은 가능하지만 MemberService의 자식 클래스인 MemberServiceImpl은 알지 못한다. 그렇기에 MemberServiceImpl로 타입 캐스팅은 불가능하다.
- JDK 동적 프록시를 사용해서 프록시를 생성하면 구체 클래스로 타입 캐스팅은 불가능하다.
- MemberServiceImpl 타입을 기반으로 JDK 동적 프록시 생성
@Test
public void cglibProxy() {
MemberServiceImpl target = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); //CGLIB 프록시
//프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
//CGLIB 프록시를 구현 클래스로 캐스팅 시도 성공
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}
- CGLIB를 사용해 프록시를 생성하고 타입 캐스팅하는 예제
- MemberServiceImpl 타입을 기반으로 CGLIB 프록시 생성
- proxyFactroy.setProxyTargetClass(true)로 CGLIB 프록시 생성
- CGLIB는 구체 클래스를 기반으로 프록시를 생성하기에 MemberServiceImpl 타입을 기반으로 프록시를 생성한다.
- CGLIB 프록시는 MemberServiceImpl 구체 클래스 기반으로 생성했기에 MemberServiceImpl 타입으로 캐스팅이 가능하고 MemberServiceImpl이 구현한 인터페이스인 MemberService 타입으로도 캐스팅이 가능하다.
- CGLIB 프록시의 부모 타입은 MemberServiceImpl이고 그 부모 타입은 MemberService로 MemberService는 프록시의 부모의 부모 타입격이다.
- MemberServiceImpl 타입을 기반으로 CGLIB 프록시 생성
- JDK 동적 프록시는 구체 클래스로 캐스팅 할 수 없고 CGLIB 프록시는 구체 클래스로 캐스팅할 수 있다.
프록시 기술과 한계 - 의존관계 주입
@Slf4j
@Aspect
public class ProxyDIAspect {
@Before("execution(* hello.aop..*.*(..))")
public void doTrace(JoinPoint joinPoint) {
log.info("[proxyDIAdvice] {}", joinPoint.getSignature());
}
}
- Aspect 예제 클래스
@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") //JDK 동적 프록시
@Import(ProxyDIAspect.class)
public class ProxyDITest {
@Autowired
MemberService memberService;
@Autowired
MemberServiceImpl memberServiceImpl;
@Test
public void go() {
log.info("memberService class={}", memberService.getClass());
log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
memberServiceImpl.hello("hello");
}
}
- JDK 동적 프록시를 사용하는 테스트 코드 예제
- properties = "~=false" : 해당 테스트에만 별도로 JDK 동적 프록시를 생성하도록 설정
- 인터페이스가 없는 경우 CGLIB로 동작한다.
- @Import(ProxyDIAspect.class) : Aspect 클래스를 스프링 빈으로 등록
- JDK 동적 프록시에 구체 클래스 타입을 주입하는 경우 발생하는 문제
- JDK 동적 프록시를 사용했는데 @Autowired로 MemberServiceImpl memberServiceImpl 구체 클래스 타입의 객체를 주입했다.
- JDK 동적 프록시를 사용하면 인터페이스를 기반으로 프록시를 생성
- 프록시는 인터페이스 기반이기에 인터페이스에 대해서는 알고 있고 인터페이스 MemberService memberService에 주입 가능하다.
- 하지만 프록시는 인터페이스를 구현한 구체 클래스 MemberServiceImpl에 대해서는 알지 못하기에 주입할 수 없고 예외가 발생한다.
- properties = "~=false" : 해당 테스트에만 별도로 JDK 동적 프록시를 생성하도록 설정
- JDK 동적 프록시를 사용하면 구체 클래스 타입에 의존관계 주입이 불가능하기에 인터페이스 타입에만 의존관계를 주입해야한다.
@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB 프록시
@Import(ProxyDIAspect.class)
public class ProxyDITest {
@Autowired
MemberService memberService;
@Autowired
MemberServiceImpl memberServiceImpl;
@Test
public void go() {
log.info("memberService class={}", memberService.getClass());
log.info("memberServiceImpl class={}", memberServiceImpl.getClass());
memberServiceImpl.hello("hello");
}
}
- CGLIB 프록시를 사용하는 테스트 코드 예제
- CGLIB는 구체 클래스를 기반으로 프록시를 생성하기 때문에 구체 클래스인 MemberServiceImpl에 대해서 알고 있을 뿐만 아니라 MemberServiceImpl이 구현한 인터페이스인 MemberService에 대해서도 알고 있다.
- 프록시는 MemberServiceImpl, MemberService 타입에 모두 의존관계 주입이 가능하다.
- CGLIB는 구체 클래스를 기반으로 프록시를 생성하기 때문에 구체 클래스인 MemberServiceImpl에 대해서 알고 있을 뿐만 아니라 MemberServiceImpl이 구현한 인터페이스인 MemberService에 대해서도 알고 있다.
- JDK 동적 프록시는 구체 클래스 타입에 의존관계를 주입할 수 없다.
- CGLIB 프록시는 구체 클래스 타입에 의존관계를 주입할 수 있다.
프록시 기술과 한계 - CGLIB
- CGLIB는 구체 클래스를 상속받아서 AOP 프록시를 생성할 때 사용한다.
- CGLIB가 구체 클래스를 상속받음으로 발생하는 문제
- 구체 클래스에 기본 생성자 필수
- 자바 문법 상 상속을 받으면 자식 클래스의 생성자를 호출할 때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 한다.
- 호출하는 부분을 생략하면 자식 클래스 생성자 첫 줄에 부모 클래스의 기본 생성자를 호출하는 super()가 자동으로 들어간다.
- CGLIB를 사용할 때 CGLIB 프록시는 구체 클래스를 상속받고 생성자에서 구체 클래스의 기본 생성자를 호출하기 때문에 구체 클래스의 기본 생성자를 만들어야 한다.
- 자바 문법 상 상속을 받으면 자식 클래스의 생성자를 호출할 때 자식 클래스의 생성자에서 부모 클래스의 생성자도 호출해야 한다.
- 생성자가 2번 호출되는 문제
- 실제 객체를 생성할 때 생성자 호출
- 프록시 객체를 생성할 때 부모 클래스의 생성자 호출
- final 키워드 클래스, 메소드 사용 불가
- final 키워드가 클래스에 있으면 상속이 되지 않는다.
- final 키워드가 메소드에 있으면 오버라이딩이 불가능하다.
=> 그렇기에 final 키워드가 있으면 프록시가 생성되지 않거나 정상 동작하지 않는다. - 웹 애플리케이션 개발 시 final 키워드를 잘 사용하지 않기에 큰 문제가 되지 않는다.
- 구체 클래스에 기본 생성자 필수
- JDK 동적 프록시는 구체 클래스 타입으로 주입할 때 문제가 발생하고 CGLIB는 구체 클래스에 기본 생성자 필수, 생성자 2번 호출 문제가 있다.
프록시 기술과 한계 - 스프링의 해결책
- 스프링 3.2 -> CGLIB를 스프링 내부에 함께 패키징
- 별도의 라이브러리 추가 없이 CGLIB 사용 가능
- 스프링 4.0 -> CGLIB 기본 생성자 필수 문제 해결, 생성자 2번 호출 문제 해결
- objenesis라는 라이브러리를 사용해서 기본 생성자 없이 객체 생성 가능하기에 기본 생성자 필수 문제와 생성자 2번 호출 문제가 해결
- 스프링 부트 2.0 -> CGLIB 기본 사용
- CGLIB를 사용함으로 구체 클래스 타입에 의존관계 주입하는 문제를 해결
- 스프링 부트는 별도의 설정이 없으면 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용
- 인터페이스가 있어도 JDK 동적 프록시가 아닌 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
spring.aop.proxy-target-classs=false
- JDK 동적 프록시 사용 방법
- application.properties 속성 추가 시 JDK 동적 프록시를 사용할 수 있다.
정리
- 프록시 내부 호출 시 발생하는 문제 - 프록시를 통해 실제 객체의 메소드를 호출하는 것이 아닌 자기 자신 내부에서 메소드를 호출하면 AOP가 적용되지 않는다.
- 실무에서 종종 발생하는 문제 중 하나로 잘 숙지해두기!!
- 프록시 내부 호출 문제 해결 방법
- 자기 자신을 의존관계로 주입받아서 호출하기
- 의존관계로 주입 받는 빈은 자기 자신이 아닌 프록시 객체이다.
- ObjectProvider를 사용해서 객체를 스프링 컨테이너에서 조회하는 시점을 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점으로 지연함으로 호출하기
- provider.getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
- 내부 호출이 발생하지 않도록 구조를 변경(분리)하기
- 내부 호출하는 메소드를 별도의 클래스로 분리해서 외부 호출이 발생하도록 한다.
- 자기 자신을 의존관계로 주입받아서 호출하기
- JDK 동적 프록시를 사용하는 경우 프록시를 구체 클래스로 타입 캐스팅 및 의존관계 주입이 불가능하다.
- JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하기 때문에 구체 클래스에 대해서 알지 못한다.
- CGLIB 프록시를 사용하는 경우 발생하는 문제점
- 구체 클래스에 기본 생성자 필수 생성
- 구체 클래스의 기본 생성자 2번 호출
- final 키워드 클래스와 메소드에 사용 불가
- 스프링이 1, 2번 문제 해결함, final 키워드는 웹 애플리케이션에서 잘 사용하지 않기에 큰 문제가 되지 않는다.
- CGLIB는 구체 클래스 타입에 의존관계 주입 문제를 해결할 수 있고 final 키워드 외에는 문제가 없기에 스프링 부트에서 기본으로 사용된다.
출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com
'Spring > [인프런 김영한 스프링 핵심 원리 - 고급편]' 카테고리의 다른 글
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP - 실전 예제 (0) | 2024.12.27 |
---|---|
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP - 포인트컷 (0) | 2024.12.27 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2024.12.17 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2024.12.16 |
[인프런 김영한 스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2024.12.15 |