[Spring] 어노테이션을 통한 AOP 설정시 주의할 점!(feat. proxy, 내부 메소드 call)
스프링은 자체적으로 프록시 패턴을 통해 비즈니스 로직과 공통 코드를 분리하는 AOP를 제공하고 있습니다.
관점지향 프로그래밍이라는 개념아래 서비스를 호출할 때 AOP의 경우 프록시 역할을 하는 객체에 의해 서비스가 호출되게 됩니다. 특정 클래스, 메소드, 어노테이션 등을 타겟으로 삼아 AOP에 쌓여진 코드는 직접 호출이 아닌 대리기사인 프록시 객체에 의해서 호출되게 되는데... 그 과정을 코드로 살펴보겠습니다.
프로젝트 구성
- AService, BService는 비즈니스 코드 역할을 할 서비스 클래스입니다. 실제 실무에서는 interface를 구현하고 있는 형태가 대부분일 것입니다.
- TestAnnotation은 커스텀으로 어노테이션을 만든 것입니다. 커스텀 어노테이션을 구현하는 방법은 다음의 내용을 참고해주세요. ( https://sas-study.tistory.com/276 )
- AspectClass는 @TestAnnotation이 붙어있는 메소드를 호출할 때 @Around 로 감싸는 코드가 들어있습니다.
AService, BService 클래스
@Service
public class AService {
@Autowired
BService bService;
public void businessLogic() {
test1();
bService.test2();
}
@TestAnnotation
public void test1() {
System.out.println("test1");
}
}
@Service
public class BService {
@TestAnnotation
public void test2() {
System.out.println("test2");
}
}
각각 test1 메소드와 test2 메소드를 가지고 이 두 메소드들은 @TestAnnotation이 붙어있다.
AspectClass 클래스
@Aspect
@Component
public class AspectClass {
@Around("@annotation(testAnnotation)")
public void test(ProceedingJoinPoint pjp, TestAnnotation testAnnotation) {
try{
System.out.println("testAnnotaion 실행");
pjp.proceed();
System.out.println("testAnnotaion 종료");
}catch (Throwable e) {
e.printStackTrace();
}
}
}
- @TestAnnotation을 가지고 있는 메소드가 실행될때 메소드 주변을 감싸는 코드이다. 실행/종료의 console만 출력하도록 하였다.
테스트 코드
@SpringBootTest
class AoptestApplicationTests {
@Autowired
AService aService;
@Test
void contextLoads() {
aService.businessLogic();
}
}
- 이런식으로 AService의 businessLogic() 메소드를 실행하면 처음 예상한 것으로는
testAnnotation 실행
test1
testAnnotation 종료
testAnnotation 실행
test2
testAnnotation 종료
이런식으로 출력되고 종료될 줄 알았다. 하지만 결과는
아니 어떻게 된 일인가...
@TestAnnotation을 붙였으나 @Around에 걸리지 않았다.
결과적으로는 여기서 프록시 패턴이 사용되었다는 것을 알 수 있다.
스프링은 각각의 서비스클래스들을 Bean이라는 객체로 관리하고 있다. 이 Bean이라는 객체들은 스프링 컨텍스트 내에서 싱글톤으로 철저하게 관리되고 있는데.. AspectJ 클래스 또한 @Component 어노테이션을 사용하여 Bean 객체로 등록을 해놓는다. 즉, 스프링이 실행하게 될때, 프록시를 이용하여 실행될 부분을 미리 알게될 것이다.(컴파일 or 클래스 로딩과정에서?)
결론적으로 말씀드리면 특정 클래스의 내부에서 호출되는 메소드(Aservice에서 호출되는 test1())는 프록시 객체에 의해서 실행되는 것이 아니게 된다. 프록시 객체는 Bean 객체의 참조를 통해서 메소드가 실행될 때 그 실행에 관여를 하게 되는데 내부적으로 실행되는 부분은 프록시를 타지 않게 된다.
고로 AService에서 모든 과정이 처리되고 프록시까지 닿지 않게되어 test1 메소드는 @Around를 타지 않았던 것이다.
역으로.. BService에서 비즈니스를 실행하고 내부 메소드를 실행할 때는 프록시를 타지 않게 된다. 하지만 AService의 참조를 이용하여 메소드를 호출할 경우에는 프록시 객체에 의해서 메소드가 실행되게 되고 @Around를 타게될 것이다.
어렵게만 생각되었던 스프링의 개념들이 점차 이해되고 머리속에 들어오기 시작하는 부분인것 같다. 앞으로도 이러한 부분을 알게된다면 많이많이 기록하고 공유하고 싶다.!!