Spring Core/03. AOP

02. Aspect 작성과 동작

  • -

이번 포스트에서는 간단한 Aspect를 작성해 보고 어떤 방식으로 동작하는지 살펴보자. 

Aspect 작성

 

테스트용 빈 등록 및 단위 테스트

퇴근까지 30분!! 퇴근 후 친구와의 약속을 떠올리며 다음의 핵심 비지니스 로직을 가지는 계산용 빈을 만들었다.

@Component
public class CalculatorBean {
    public double divide(int a, int b){
        return a/b;
    }

    public int add(int... args){
        return Arrays.stream(args).sum();
    }
}


이제 위 빈이 잘 동작하는지 테스트 해보자. 테스트만 통과하면 퇴근 각이다.!

@SpringBootTest
@Slf4j
@ActiveProfiles("dev")
public class CalculatorBeanTest {
    @Autowired
    CalculatorBean bean;

    @Test
    public void divideTest(){
        double result =bean.divide(100, 2);
        Assertions.assertEquals(50, result);
    }

    @Test
    public void addTest(){
        int result = bean.add(1,2,3);
        Assertions.assertEquals(6, result);
    }

    @Test
    public void beanTypeTest(){
        Class<?> clazz = bean.getClass();
        Assertions.assertEquals(CalculatorBean.class, clazz);
        log.debug("bean type: {}", clazz.getName());
    }
}


단위테스트는 잘 통과할 것이다. 여기서 주의 깊게 살펴볼 부분은 testBean에서 출력한 log 부분이다. 물론 실제 구현체가 CalculatorBean이었기 때문에 이상한 점은 하나도 없다.

06:59:17 [DEBUG] c.d.a.s.CalculatorBeanTest.beanTypeTest.38: 
                 bean type: com.doding.aop.simple.CalculatorBean


아무튼 우리가 만든 멋진 비즈니스 로직들은 잘 동작한다.  그런데 새로오신 부장님이 한마디 쓱 던지고 가신다.

"메서드에 전달된 argument들을 로깅해야하지 않겠어??" 설마 이제까지 하나도 안한건 아니지?


아뿔싸!! 하나도 안했다. 로깅과 같은 횡단 관심사 없이도 핵심 로직이 잘 동작해서 간과한 것이다. 약속시간 10분 전! 일을 잘 마무리 하고 친구를 만날 수 있을까?

도와줘요 AOP!!

 

Aspect 작성

 

dependency 추가

aop를 위해서 추가적인 라이브러리가 필요하다. www.mvnrepository.com에서 spring boot starter aop를 검색해 보자.

spring boot starter aop

최신 버전 선택 후 maven용 코드를 복사해서 pom.xml에 추가한다. 물론 이때 version 정보는 삭제한다. 대부분 의존성의 버전은 Spring Boot가 관리한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
  <!-- <version>3.2.2</version> -->
</dependency>
라이브러리를 사용할 때 버전 정보를 삭제할지 말아야 할지 헷갈린다면 일단 지워보고 지웠을 때 아래와 같이 오류가 표시된다면 다시 버전을 입력하자.
commons-io는 SpringBoot에서 관리하지 않는다.

 

@Aspect를 이용한 aspect 작성

다음과 같이 Aspect를 만들어보자.

Aspect의 구성

  • CommonAspect는 aspect를 담고 있는 빈으로 구성하기 위해 @Component와 @Aspect를 선언한다. 일반적으로 Aspect 안에 있는 메서드 하나가 advice 하나를 의미한다.
  • advice는 advice 타입과 advice 코드로 구성된다.  
    • @Before는 advice의 타입으로 advice의 실행 시점을 의미하는데 'Before'니까 타겟의 메서드가 실행되기 전을 의미한다.
    • 메서드 바디에는 실행할 advice 코드 즉 횡단 관심사를 작성한다. 여기서는 JoinPoint를 이용해서 AOP가 적용되는 메서드 즉 target method의 signature를 출력할 계획이다.
  • advice type을 나타내는 @Before의 value 속성으로는 pointcut을 설정한다.
    • pointcut은 대상 메서드(join point)를 필터링한다. pointcut 작성 방법은 뒤에 상세히 다룬다.

 

더보기

참고로 만약 legacy spring을 사용한다면 추가로 EnableAspectJAutoProxy라는 애너테이션이 설정에 추가되어야 한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;
}

 

다시 한번 단위 테스트를

Aspect 작성이 끝났다면 다시 한번 다시 한번 단위테스트를 실행해 보고 바뀐 점을 살펴보자.

07:23:17 [TRACE] c.d.a.a.CommonAspect.argumentLogging.15: 
       method call: int com.doding.aop.beans.CalculatorBean.divide(int,int), [100, 2]
07:26:15 [TRACE] c.d.a.a.CommonAspect.argumentLogging.15: 
       method call: int com.doding.aop.beans.CalculatorBean.add(int[]), [[1, 2, 3]]


테스트 결과를 살펴보면 놀랍게도 divide, add가 호출될 때마다 우리가 설정해 놓은 AOP가 잘 동작함을 알 수 있다. 어떻게 이런 마법 같은 일이 가능할까? 아주 신기해 하며 3번째 테스트를 실행하는 순간 좌절에 빠지고 만다. 테스트는 실패다!

org.opentest4j.AssertionFailedError: 
Expected :class com.doding.aop.beans.CalculatorBean
Actual   :class com.doding.aop.beans.CalculatorBean$$SpringCGLIB$$0

하지만 실패의 원인도 가늠하기 어렵다. CalculatorBean$$SpringCGLIB$$0은 도대체 뭐지?

 

AOP의 비밀 proxy

 

proxy란?

AOP의 비밀은 proxy 객체가 가지고 있다. 

proxy란 '대리/대신' 이런 뜻을 가지고 있는데 겉에서 봤을 때는 원본(target)과 똑같이 생겨서 구분할 수 없고 런타임에 target 메서드 호출 전/후 등에 끼어들어서 무언가 부가적인 작업을 하는 객체를 의미한다. 

이 proxy는 빈이 interface 기반인지 또는 class 기반인지에 따라서 생성 방식이 약간 다르다.

  interface 기반 빈에 대한 Proxy class 기반 빈에 대한 Proxy
종류 JDK의 동적 프록시 CGLIB(Code Generator Library)의 프록시
구성
장/단점 생성 시간이 빠르지만 인터페이스가 반드시 필요 클래스 바이트 코드를 조작하므로 생성 시간이 길지만 언제나 적용 가능

결과적으로 어떤 방식으로 Proxy를 만들던지 동작은 동일하며 proxy 클래스는 target 빈의 메서드를 재정의 하기 때문에 외부에서는 proxy의 메서드를 부르는지 target 빈의 메서드를 부르는지 알지 못한다.

참고로 SpringBoot에서는 CGLIB를 기본으로 설정할 것인지에 대한 옵션으로 spring.aop.proxy-target-class 속성이 있는데 기본값은 true이다. 아마 생성되는 proxy에 대해 일관성을 갖기 위한 설정이 아닌가 한다.

 

proxy의 동작 방식

스프링은 빈 생성 후 pointcut에 기반해서 AOP 적용 대상인지 검토하고 대상이라면 proxy 클래스를 생성한 후 기존의 빈 즉 target 대신 proxy를 노출한다. 

target에서 proxy로의 새대 교체

따라서 ApplicationContext에 빈을 요청하면 proxy가 반환된다. 이제 빈의 메서드를 호출하면 proxy가 target으로 가는 요청을 가로챈 후 @Before, @After 등 호출 시점에 맞춰 target의 메서드 호출 전/후에 advice 코드를 실행한다.

aop의 동작 과정

 

https://inf.run/uTES7

 

참~쉬운 스프링 Part 1. 스프링 프레임워크 이해 및 DI,AOP 강의 | 모두의 코딩:두딩 - 인프런

모두의 코딩:두딩 | 스프링 프레임워크의 핵심을 파고드는 강의로, DI와 AOP를 비롯한 스프링의 중심 개념을 마스터하게 됩니다. 환경 설정부터 단위테스트, 로깅, 메이븐 사용법까지, 스프링 프

www.inflearn.com

 

'Spring Core > 03. AOP' 카테고리의 다른 글

05. Spring 내부의 AOP들  (0) 2021.10.29
04. advice의 타입  (0) 2020.06.20
03. pointcut 작성  (0) 2020.06.20
01. AOP 기본 컨셉  (0) 2020.06.18
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.