[Spring] AOP 주의사항
스프링은 프록시로 AOP를 적용한다.
스프링 컨테이너는 실제 객체 대신 프록시를 스프링 빈으로 등록해 실제 객체를 직접 호출하는 문제가 발생하지 않을 것 같지만... 실제 객체 내부에서 메서드 호출 시 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생할 수 있다.
@Slf4j
@Component
public class CallServiceV0 {
public void external() {
log.info("call external");
internal(); //내부 메서드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* start.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
AOP external과 internal 메서드 모두 프록시가 실제 객체 대신 빈으로 등록된다.
하지만.. external 메서드를 실행 시 내부에서 internal 메서드를 실행하게 되는데 이 때 실행되는 internal 메서드는 프록시가 아니고 실제 메서드이다.
자신의 내부 메서드를 호출하는 경우 this 키워드가 붙게 되고 내부 호출은 프록시를 거치지 못해 어드바이스를 적용할 수 없다.
여러 가지 방법으로 해당 문제를 해결할 수 있다.
@Component
public class CallService {
private CallService callService;
@Autowired
public void setCallService(CallService callService) {
this.callService = callService;
}
public void external() {
log.info("call external");
callService.internal();
}
}
수정자로 Service를 주입받는다.
이 때 주입받는 대상은 프록시 객체이니 AOP를 정상적으로 적용할 수 있다.
생성자로 주입받을 시 생성하면서 주입을 동시에 수행해야 하기에 주입에 실패한다.
따라서 위와 같이 수정자 주입을 사용하거나 ApplicationContext나 ObjectProvider를 사용해 지연 조회를 사용하면 된다.
@Component
public class CallService {
// private final ApplicationContext applicationContext;
private final ObjectProvider<CallService> callServiceProvider;
public CallService(ObjectProvider<CallService> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
log.info("call external");
CallService callService = callServiceProvider.getObject();
callService.internal();
}
public void internal() {
log.info("call internal");
}
}
ApplicationContext는 너무 무거우니 ObjectProvider로 실제 객체를 사용할 때 컨테이너에서 조회하도록 지연시키자.
@Slf4j
@Component
@RequiredArgsConstructor
public class CallService {
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");
}
}
가장 좋은 방법은 구조를 바꿔서 내부 호출 자체를 삭제하는 방법이다.
AOP가 잘 적용되지 않는다면 혹시 public 메서드에서 public 메서드를 호출하는 것처럼 메서드의 내부 호출이 발생하는지 확인해보자.
스프링은 JDK 동적 프록시와 CGLIB 프록시 기술 중 CGLIB 기술을 사용한다.
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 만들고, CGLIB는 구현체를 기반으로 프록시를 만든다.
JDK 동적 프록시 기술로 만들어진 프록시 객체는 인터페이스를 알고 있지만 인터페이스의 구현체에 대해서는 알지 못한다.
따라서 빈을 등록할 때 JDK 동적 프록시 기술을 사용한다면 MemberServiceImpl 타입을 DI 받을 수 없다.
CGLIB는 구현체를 기반으로 프록시를 만들기에 이런 제약이 없지만... CGLIB도 단점이 있다.
CGLIB로 만드는 프록시 객체는 구현체 클래스를 상속받는다.
따라서 프록시 객체를 생성할 때 부모 클래스의 생성자도 호출해야 한다. (CGLIB는 기본 생성자를 호출한다)
즉 구현체 클래스에 기본 생성자를 만들어 줘야 하고, 이 생성자는 2번 호출된다.
실제 구현 클래스를 상속받는 프록시 객체를 동적으로 생성할 때 한 번, 스프링 빈 생성 과정에서 한 번 호출해 총 2번 호출된다.
그리고 상속을 사용하다 보니 final 키워드가 붙어있으면 CGLIB 기술을 사용할 수 없다는 제약이 있다.
그럼에도 CGLIB를 기본으로 사용하는 이유가 뭘까?
스프링 4.0 버전 이후부터는 objenesis 라이브러리를 사용해 기본 생성자 없이 객체를 생성할 수 있게 됐다.
따라서 생성자가 2번 호출되는 문제는 해결됐다.
final 이 붙은 경우 사용할 수 없다는 제약은 해결하지 못했지만..
AOP의 적용 대상에는 final을 잘 사용하지 않으니 크게 문제되지는 않는다.
'Spring > Spring' 카테고리의 다른 글
[Spring] 포인트컷 지시자 (0) | 2023.07.28 |
---|---|
[Spring] AOP 적용 (0) | 2023.07.27 |
[Spring] 빈 후처리기 (0) | 2023.07.26 |
[Spring] 프록시 팩토리 (0) | 2023.07.24 |
[Spring] 동적 프록시 기술 (0) | 2023.07.23 |
댓글
이 글 공유하기
다른 글
-
[Spring] 포인트컷 지시자
[Spring] 포인트컷 지시자
2023.07.28 -
[Spring] AOP 적용
[Spring] AOP 적용
2023.07.27 -
[Spring] 빈 후처리기
[Spring] 빈 후처리기
2023.07.26 -
[Spring] 프록시 팩토리
[Spring] 프록시 팩토리
2023.07.24