-
AOP 주의사항 - 프록시와 내부 호출스프링/스프링 AOP 2023. 9. 23. 23:27
스프링은 프록시 방식의 AOP를 사용하기 때문에 AOP를 적용하기 위해서는 항상 프록시를 통해서 대상 객체를 호출해야 한다.
AOP 를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다. 스프링은 의존관계 주입시에 항상 프록시 객체를 주입하기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않지만 대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생하는 경우가 있다.
이해하기 쉽게 코드로 설명하겠다.
CallLogAspect.class
@Slf4j @Aspect public class CallLogAspect { @Before("execution(* hello.aop.internalcall..*.*(..))") public void doLog(JoinPoint joinPoint) { log.info("aop={}", joinPoint.getSignature()); } }
aop로 사용하게 되는 CallLogAspect이다. 포인트컷의 범위는 internalcall 패키지의 아래에 있는 패키지와 클래스 전부이다. 아래의 CallServiceV0의 메서드 전부가 aop로 적용된다.
CallServiceV0.class
@Slf4j @Component public class CallServiceV0 { public void external() { log.info("call external"); internal(); // 내부 메서드 호출(this.internal()) } public void internal() { log.info("call internal"); } }
external() 메서드의 내부에서 internal()을 호출한다. 이 때, 문제가 발생할 것이다.
@Slf4j @Import(CallLogAspect.class) @SpringBootTest class CallServiceV0Test { @Autowired CallServiceV0 callServiceV0; @Test void external() { callServiceV0.external(); } @Test void internal() { callServiceV0.external(); } }
여기서 external()을 호출되면 external의 프록시 -> external의 실제 객체 -> internal의 프록시 -> internal의 실제 객체 순으로 호출되는 것으로 예상되어야 하는데 결과는 그렇지 않다.
external을 호출할 때에는 프록시가 호출된 다음에 실제 객체가 호출되었지만, internal은 바로 실제 객체가 호출된다.
이 문제가 바로 프록시의 내부호출 문제이며 실무에서 많이 발생하는 문제이다.
internal이 호출될 때에는 aop가 호출되지 않는 이유는 자기 자신의 인스턴스를 가리키기 때문이다.
자바 언어에서 메서드 앞에 별도의 참조가 없으면 this라는 뜻으로 자기 자신의 인스턴스를 가리킨다. external() 메서드 안에있는 internal()의 호출은 this.internal()과 같은 것이다.
여기서 this는 실제 대상 객체의 인스턴스를 뜻하므로 이러한 내부 호출은 프록시를 거치지 않아 어드바이스도 적용할 수 없다.
프록시 방식의 AOP 한계
스프링은 프록시 방식의 AOP를 사용하기 때문에 메서드 내부 호출에 프록시를 적용할 수 없다는 한계점이 있다.
한계점 대안 1 - 자기 자신 주입
@Slf4j @Component public class CallServiceV1 { private CallServiceV1 callServiceV1; @Autowired public void setCallServiceV1(CallServiceV1 callServiceV1) { this.callServiceV1 = callServiceV1; } // 그냥 생성자를 만들어버리면 생성되지도 않은 시점에서 사용되기에 무한 순환 에러 발생 -> setter로 만들기 public void external() { log.info("call external"); callServiceV1.internal(); //외부 메서드 호출 } public void internal() { log.info("call internal"); } }
생성자를 그냥 만들어주면 생성되지도 않은 시점에서 사용되기에 무한 순환 에러가 발생하여서 위에처럼 setter로 만들어줘야 한다.
spring.main.allow-circular-references=true
물론 이것도 그냥 사용하면 안되고 application.properties에 위 코드를 넣어줘야한다.
자기 자신을 private final로 가진 다음에 그 객체의 internal() 메서드를 호출하는 것이다.
여기서 자기 자신은 실제 객체가 아니라 프록시 객체이기 때문에 내부 호출에서도 프록시 객체를 먼저 호출한다.
스프링 컨테이너에는 프록시가 들어있기 때문에 external()의 프록시, 실제 객체를 호출한 다음에 this.internal()이 아닌 callService.internal()을 호출하기 때문에 internal()의 프록시 -> internal()의 실제 객체를 호출한다.
실제 객체 호출 전에 프록시가 잘 호출된다.
한계점 대안 2 - 지연 조회
위의 대안 1에서 생성자 주입이 안된다고 했던 것은 자기 자신을 생성하면서 주입하는 것에 무한 로딩이 걸려서 에러가 나기 때문이다. 대안 1에서는 수정자 주입을 사용했는데, 지금은 지연 조회를 사용하는 방법이다.
말 그대로 스프링 빈을 지연해서 조회하는 방법이다.
ObjectProvider(Provider)나 ApplicationContext를 사용하면 된다.
@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를 사용해서 지연 조회 방법으로 외부 메서드를 호출하는 방식이다. callServiceProvider.getObject()는 컨테이너에서 스프링 빈을 꺼내기 때문에 컨테이너에 주입된 프록시를 호출해서 내부 호출이 아니라 외부 호출이 되는 것이다.
ObjectProvider는 객체를 스프링 컨테이너에 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연하기 때문에 지연 조회 방법이다. 실행하면 aop를 잘 실행한다.
한계점 대안 3 - 구조 변경
앞선 대안들은 자기 자신을 주입하거나 Provider를 사용하는 것처럼 자연스럽지 않은 방식들이었다. 구조 변경은 가장 나은 대안으로 내부 호출이 발생하지 않도록 구조를 변경하는 것이다. 이 방법이 가장 많이 쓰이며 가장 권장하는 방법이라고 한다.
@Slf4j @Component public class InternalService { public void internal() { log.info("call internal"); } }
@Slf4j @Component @RequiredArgsConstructor public class CallServiceV3 { private final InternalService internalService; public void external() { log.info("call external"); internalService.internal(); } }
InternalService를 만들어서 완전히 다른 클래스로 분리하는 것이다. 그리고 CallServiceV3에서 internal이 있는 InternalService를 주입받아서 internal을 호출하는 것이다.
클래스가 완전이 다르니까 외부 메서드 호출이 이루어지는 것이다.
위에서 설명했듯이 아예 다른 클래스이므로 내부 호출 문제가 발생하지 않는다.
내부 호출 문제는 개발을 하다가 자주 만날 수 있는 문제점이라고 한다. 문제가 발생했을 때 해결하기도 쉽지 않기 때문에 내부 호출 문제의 정의를 잘 파악하고 있다가 설계를 할 때 조심해서 해야된다.
3가지 대안 중에서 3번째 대안이 가장 좋아서 만약에 문제를 발견하게 된다면 구조 변경 방법을 사용하는 것이 좋다.
'스프링 > 스프링 AOP' 카테고리의 다른 글
HTTP 요청 응답 기록 - httpexchanges (0) 2023.10.01 재시도 AOP (0) 2023.09.23 어노테이션으로 AOP 사용 (0) 2023.09.23 포인트컷 - execution (0) 2023.09.23 어드바이스 종류 (0) 2023.09.22