본문 바로가기
Spring|Spring-boot/Spring AOP

Spring AOP 한계

by oncerun 2022. 1. 31.
반응형

스프링에서 제공하는 AOP를 사용하면서 기억해야 할 것들

 

스프링 AOP는 프록시프락시 방식의 AOP만을 사용한다.  따라서 AOP를 적용하기 위해서는 프록시를 통해 실제 Target에 접근해야 하는데, 이 과정에서 프록시에서 Advice를 실행한 후 대상 객체를 호출하게 된다.

 

만약 대상 객체가 포인트 컷을 통해 매칭될 시 스프링 빈으로 프록시 객체가 등록된다. 따라서 스프링은 해당 객체의 의존관계 주입 시 프록시 객체를 주입해주기 때문에 대부분 대상 객체 직접 호출 문제는 발생하지 않는다.

하지만 대상 객체 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 있다.

 

간단한 테스트를 진행하자

 

@Slf4j
@Service
public class InternalCallService {

    public void external() {
        log.info("external call");
        internal();
    }

    public void internal() {
        log.info("internal call");
    }

    @Aspect
    static class ExternalAspect {

        @Before("execution(* hello.aop.internal.InternalCallService.external(..))")
        public void externalCall(JoinPoint joinPoint) {
            log.info("joinPoint signature = {} external Call", joinPoint.getSignature());
        }

        @Before("execution(* hello.aop.internal.InternalCallService.internal(..))")
        public void internalCall(JoinPoint joinPoint) {
            log.info("joinPoint signature = {} internal Call", joinPoint.getSignature());
        }

    }

}

 

external()에서 내부 메서드인 internal()을 호출하고 있으며, 각 메서드 호출마다 log를 찍도록 하였다. 

 

예상하는 기댓값은 다음과 같다.

 

1. 클라이언트가 external() 메서드 호출 -> AOP 적용하여 external Call 로그를 남긴다.

 

2. 내부 메서드인 internal()메서드를 external()에서 호출한다. 

 

3. internal()메서드가 실행되기 이전 log가 남는다.

 

@SpringBootTest
@Import(InternalCallService.ExternalAspect.class)
class InternalCallServiceTest {

    @Autowired
    InternalCallService internalCallService;

    @Test
    void test() {
        internalCallService.external();
    }

}

 

테스트 결과로그

joinPoint signature = void hello.aop.internal.InternalCallService.external() external Call
external call
internal call


예상과는 다르게 internal() 메서드를 실행 전에 어드바이스가 적용이 안된 것을 확인할 수 있다.

클라이언트가 호출한 external()은 AOP가 적용되어 프록시 객체가 위임받을 것이고, 그 이후 어드바이스 적용 -> 실제 타깃 호출 -> 내부 메소드 실행 

 

 

 

해결 방안

 

  • 스프링 AOP는 프록시 객체를 사용하기 때문에 고려할 수 있는 첫 번째 방법은 AspectJ 직접 사용하는 방법이다.
    - 로드 타임 위빙, JVM설정 등 고려할 점이 많아 추천하지 않는다.
  • 자기 자신을 주입받는 방법
    private final InternalCallService internalCallService;  
    
    @Autowired
    public InternalCallService(InternalCallService internalCallService) {
        this.internalCallService = internalCallService;
    }​
    다음과 같이 주입받으면 순환 사이클 문제가 발생한다. 자신이 생성되지 않았는데 어떻게 주입을 받지?

    스프링은 생성하는 단계 이후 setter로 주입하는 단계가 분리되어 있기 때문에 다음과 같이 setter로 주입을 받아야 한다.
    (스프링 부트 2.6 기준 순환 참조를 기본적으로 막는 정책이 존재한다.)
    application.properties에 spring.main.allow-circular-references=true를 추가하자.
    private InternalCallService internalCallService;
    
    @Autowired
    public void setInternalCallService(InternalCallService internalCallService) {
        this.internalCallService = internalCallService;
    }​

    테스트 결과 로그

    joinPoint signature = void hello.aop.internal.InternalCallService.external() external Call
    external call
    joinPoint signature = void hello.aop.internal.InternalCallService.internal() internal Call
    internal call​
  •  지연 조회
    ObjectProvier  혹은 ApplicationContext 를 통해 스프링 컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아닌 실제 객체를 사용하는 시점으로 지연하여 사용할 수 있다.
    private final ObjectProvider<InternalCallService> internalCallServiceObjectProvider;
    
    public InternalCallService(ObjectProvider<InternalCallService> internalCallServiceObjectProvider) {
        this.internalCallServiceObjectProvider = internalCallServiceObjectProvider;
    }
    
    public void external() {
        log.info("external call");
        InternalCallService internalCallService = internalCallServiceObjectProvider.getObject();
        internalCallService.internal();
    }
    
    public void internal() {
        log.info("internal call");
    }​
  • 구조 변경

    자신을 주입받거나, Provierder를 사용하지 않도록 구조를 변경하는 것을 스프링에서는 권장한다.

    즉 해당 메서드를 별도의 클래스로 빼서 주입받아 사용하는 것이다.
    @Slf4j
    @Component
    public class InternalService {
        public void internal() {
            log.info("internal call");
        }
    }​

    내부 메서드를 별도의 클래스로 지정하고 해당 클래스를 스프링 빈으로 주입받아 사용하자.
    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class InternalCallService {
    
        private final InternalService internalService;
    
        public void external() {
            log.info("external call");
            internalService.internal();
        }
    
    }​
     
     

결과적으로 이러한 오류를 방지하기 위해선 프록시 대상이 되는 객체를 다룰 때 내부 메서드를 사용한다면 AOP가 적용되는지 꼭 한번 생각해야 한다는 것이다.

 

 

 

 

반응형

'Spring|Spring-boot > Spring AOP' 카테고리의 다른 글

Spring AOP 한계(2)  (0) 2022.10.14
Spring AOP 매개변수 활용  (0) 2022.01.30
Spring AOP @target, @within  (0) 2022.01.29
Pointcut - within, args  (0) 2022.01.29
Pointcut - execution  (0) 2022.01.29

댓글