본문 바로가기
IT 개인 공부/Spring

[Spring] AOP가 적용되지 않은 메서드에서 AOP가 적용된 메서드를 호출하면 AOP가 정상적으로 동작할까?

by Libi 2021. 9. 11.
반응형

Spring AOP를 공부하던 도중 "AOP가 적용되지 않은 메서드에서 AOP가 적용된 메서드를 호출하면 AOP가 정상적으로 동작할까?"라는 의문을 가지게 되었다.

 

이를 알아보기 위해 코드를 작성하고 여러 가지 테스트를 실행해보자.

먼저 어노테이션 기반의 단순한 @TestExecution을 선언하자.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestExecution {
}

@Aspect
@Component
public class TestAspect {

    @Around("@annotation(TestExecution)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("=============");
        Object proceed = joinPoint.proceed();
        System.out.println("=============");
        return proceed;
    }
}

 

AOP를 구현하였으니 실제 코드에 적용시켜서 실행시켜보도록 하자.

AOP가 걸려있지 않은 fooA가 AOP가 걸린 fooB를 내부적으로 호출하는 상황이다.

@RestController
public class AopTest {

    @GetMapping("/fooA")
    public void fooA() {
        System.out.println("fooA");
        fooB();
    }

    @TestExecution
    @GetMapping("/fooB")
    public void fooB() {
        System.out.println("fooB");
    }
}

 

우리가 기대하는 출력은 다음과 같다.

하지만 실제로 동작해보면 기대와는 달리 AOP가 정상적으로 동작하지 않는다는 것을 알 수 있다.

 

이를 해결하기 위해 스택오버플로우에서 찾아본 결과 다음과 같이 스프링 컨테이너를 통해 주입받은 AopTest 객체로 fooB를 호출하면 AOP가 정상적으로 동작한다는 것을 확인하였다.

@RestController
public class AopTest {

    @Autowired
    private AopTest aopTest;
    
    @GetMapping("/fooA")
    public void fooA() {
        System.out.println("fooA");
        aopTest.fooB(); //주입받은 aopTest 객체로 fooB를 호출하면 정상적으로 AOP가 동작
    }

    @TestExecution
    @GetMapping("/fooB")
    public void fooB() {
        System.out.println("fooB");
    }
}

 

 

왜 전자는 제대로 동작하지 않고 후자는 정상적으로 동작하는 것일까? 이는 AOP 메커니즘을 이해하면 알 수 있다.

Spring은 AOP를 처리하기 위해서 CGLIB라는 바이트 코드 조작 라이브러리를 통해 AopTest@CGLIB 클래스를 생성하여 스프링 컨테이너에 빈으로 등록하며, 프록시 패턴을 적용하여 AopTest 클래스가 수행하는 로직을 대신 처리해준다.

 

내 생각으로는 아마 CGLIB가 적용된 AopTest@CGLIB는 다음과 비슷한 느낌으로 구현되어있을 것이다.

@RestController
public class AopTest@CGLIB {
    
    @GetMapping("/fooA")
    public void fooA() {
        System.out.println("fooA");
        fooB();
    }

    //AOP 코드가 실제로 적용
    @GetMapping("/fooB")
    public void fooB() {
        System.out.println("============");
        System.out.println("fooB");
        System.out.println("============");
    }
}

 

즉, 순수한 AopTest 객체가 아닌 프록시 객체인 AopTest@CGLIB 객체로 fooB() 메서드를 호출해야지만 AOP가 적용된 결괏값을 얻을 수 있다는 것을 예상할 수 있다.

 

전자인 경우 fooB()를 호출하는 객체는 순수한 AopTest 객체(this)이다.

순수한 AopTest 객체의 fooB()는 AOP가 적용되지 않은 메서드이기 때문에 AOP가 동작하지 않고 단순히 fooB() 메서드만 실행되는 것이다.

//순수한 AopTest 클래스의 fooB()
public void fooB() {
	System.out.println("fooB");
}

 

반면, 후자인 경우는 스프링 컨테이너로부터 프록시 객체(AopTest@CGLIB)를 주입받은 aopTest 객체를 통해 fooB()를 호출하기 때문에 AOP가 적용된 fooB() 메서드를 호출하여 AOP가 정상적으로 동작하는 것이다.

//CGLIB가 적용된 AopTest 클래스의 fooB()
public void fooB() {
     System.out.println("============");
     System.out.println("fooB");
     System.out.println("============");
}

 

실제로 주입받은 aopTest 객체를 출력해보면 순수한 AopTest 클래스가 아닌 CGLIB가 적용된 클래스인 것을 확인할 수 있다.

 

 

AOP 기반의 @Transactional도 마찬가지로 주입받은 aopTest 객체를 통해 fooB()를 호출하면 정상적으로 동작하지만, 순수한 aopTest로 fooB()를 호출하면 정상적으로 동작하지 않는 것을 확인할 수 있다.

@RestController
public class AopTest {

    @Autowired private EntityManager em;
    @Autowired private AopTest aopTest;

    @GetMapping("/fooA")
    public void fooA() {
        aopTest.fooB(); //성공
        fooB(); //실패
    }

    @Transactional
    @GetMapping("/fooB")
    public void fooB() {
        em.persist(new Member());
    }
}

 

 

 

 

[ Reference ]

· https://stackoverflow.com/questions/13564627/spring-aop-not-working-for-method-call-inside-another-method 

반응형

댓글