Framework/Spring

[Spring] Spring에서 AOP는 어떻게 사용할까?

KOOCCI 2022. 8. 13. 07:51

목표 : Spring에서 AOP를 사용할 수 있다.


앞서, AOP가 무엇인지에 대해 알아보았다.

이제 Spring에서 AOP를 어떻게 쓸 수 있을지 알아보자.

Aspect Oriented Programming with Spring

다음 링크에서 Spring에서 AOP에 대한 적용은 다음과 같이 할 수 있다고 한다.

 

Spring provides simple and powerful ways of writing custom aspects by using either a schema-based approach or the @AspectJ annotation style. Both of these styles offer fully typed advice and use of the AspectJ pointcut language while still using Spring AOP for weaving.

즉, schema-based approach를 사용할 수도 있고, @AspectJ라는 Annotation을 사용할수도 있다고 한다.

 

Schema-based AOP Support

Annotation은 소스가 분산되어, 이해를 돕기위해 Schema-based 방식을 먼저 보도록 하자.

먼저, XML로 AOP를 설정하려면 aop Schema를 설정해주어야 한다.

 

Declaring an Aspect

그럼 Aspect부터 설정해보자.

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->
    <aop:config>
        <aop:aspect id="myAspect" ref="aBean">
        </aop:aspect>
    </aop:config>

    <bean id="aBean" class="com.example.demo.aop.AopBean">
    </bean>

</beans>

예시에 따라 구성하였고, AopBean 클래스를 만들어, Bean 설정에 추가해주자.

 

Declaring a Pointcut

Pointcut도 마찬가지로 설정할 수 있다.

Pointcut은 aspect 안에 존재한다.

<aop:config>

    <aop:pointcut id="businessService"
        expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>

바로 작성하기 전에, 위 예시를 보면, expression이 존재한다.

execution(* com.xyz.myapp.service.*.*(..))

excution 할 때 (Runtime 때) 특정 패키지(com.xyz...service)의 모든 클래스(service.*), 모든 메서드(service.*.*), 그리고 모든 Argument(*.*.(..))에 해당한다는 표현식이다.

 

그럼 실제로 적용해보자.

package com.example.demo.aop;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AopBean {
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->
    <aop:config>
        <aop:aspect id="myAspect" ref="aBean">
                <aop:pointcut id="businessService" expression="execution(* com.example.demo.aop.AopBean.*.*(..))"/>
        </aop:aspect>
    </aop:config>

    <bean id="aBean" class="com.example.demo.aop.AopBean">
    </bean>

</beans>

 

Declaring Advice

이제 Advice를 적용해보자.

 

package com.example.demo.aop;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AopBean {
    public void beforeLog() {
        log.error(">>> before aop log");
    }

    public void afterLog() {
        log.error(">>> after aop log");
    }

    public void afterReturningLog() {
        log.error(">>> afterReturningLog aop log");
    }

    public void afterThrowingLog() {
        log.error(">>> afterThrowingLog aop log");
    }
}
package com.example.demo.aop;

import lombok.extern.slf4j.Slf4j;


@Slf4j
public class Service {
    public void log() {
        log.error(">>> aop log");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->
    <aop:config>
        <aop:aspect id="myAspect" ref="aBean">
            <aop:pointcut id="businessService" expression="execution(* com.example.demo.aop.Service.*(..))"/>
            <aop:before pointcut-ref="businessService" method="beforeLog"/>
            <aop:after pointcut-ref="businessService" method="afterLog"/>
            <aop:after-returning pointcut-ref="businessService" method="afterReturningLog"/>
            <aop:after-throwing pointcut-ref="businessService" method="afterThrowingLog"/>
        </aop:aspect>
    </aop:config>

    <bean id="aBean" class="com.example.demo.aop.AopBean">
    </bean>

    <bean id="service" class="com.example.demo.aop.Service">
    </bean>

</beans>
package com.example.demo.aop;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        Service service = context.getBean(Service.class);
        service.log();
        context.close();
    }
}

혹시, Class Not Found 에러가 난다면, 아래 2개 Dependency를 추가해주자. (Springboot는 starter-aop 가 따로 있다)

Aspectj Runtime

aspectJ Weaver

 

04:39:29.454 [main] ERROR com.example.demo.aop.AopBean - >>> before aop log
04:39:29.466 [main] ERROR com.example.demo.aop.Service - >>> aop log
04:39:29.466 [main] ERROR com.example.demo.aop.AopBean - >>> after aop log
04:39:29.466 [main] ERROR com.example.demo.aop.AopBean - >>> afterReturningLog aop log

그 결과를 위와 같이 확인해 볼 수 있다.

 

Around Advice

 

그럼, 위에서 했던 것들을 한번에 해주는 Around Advice를 보자.

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around
        pointcut-ref="businessService"
        method="doBasicProfiling"/>

    ...
</aop:aspect>
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

Around에서는 method에서 Argument를 받는다.

 

앞선 포스팅에서 확인해 보았듯이, Proxy로 Sample 예제를 보았을 때와 동일하다.

 

package com.example.demo.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;

@Slf4j
public class AopBean {
    public void aroundLog(ProceedingJoinPoint pjp) {
        log.error(">>> before aop log");
        try {
            Object proceed = pjp.proceed();
            log.error(">>> returning aop log");
        } catch (Throwable throwable) {
            log.error(">>> throwing aop log");
        }
        log.error(">>> after aop log");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->
    <aop:config>
        <aop:aspect id="myAspect" ref="aBean">
            <aop:pointcut id="businessService" expression="execution(* com.example.demo.aop.Service.*(..))"/>
            <aop:around pointcut-ref="businessService" method="aroundLog"/>
        </aop:aspect>
    </aop:config>

    <bean id="aBean" class="com.example.demo.aop.AopBean">
    </bean>

    <bean id="service" class="com.example.demo.aop.Service">
    </bean>

</beans>

 

@AspectJ

그럼 이제, Annotation을 통해, 처리하는 것으로 변경해보도록 하자.

@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class AppConfig {
}

먼저, AspectJ는 외부 라이브러리이므로, Configuration에 위와 같이 넣어줄 필요가 있다.

@Slf4j
@Component
public class Service {
    public void log() {
        log.error(">>> aop log");
    }
}

@Slf4j
@Aspect
@Component
public class AopBean {

    @Pointcut("execution(* com.example.demo.aop.Service.*(..))")
    public void aopPointcut(){}

    @Around("aopPointcut()")
    public Object aroundLog(ProceedingJoinPoint pjp) {
        log.error(">>> before aop log");
        try {
            Object proceed = pjp.proceed();
            log.error(">>> returning aop log");
            return proceed;
        } catch (Throwable throwable) {
            log.error(">>> throwing aop log");
        }
        log.error(">>> after aop log");
        return null;
    }
}
package com.example.demo.aop;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AppConfig.class);
        context.refresh();

        Service service = context.getBean(Service.class);
        service.log();
        context.close();
    }
}

XML 파일을 없애더라도, 정상 동작하는 것을 알 수 있다.

@Aspect 안에, Pointcut을 설정하였고, @Around로 Advice도 적용되었다는 것을 알 수 있다.

상황에 따라, Pointcut을 별도로 정리하고, Advice에는 여러 Pointcut을 적용하는 등의 동작도 가능하다.

 

Wrap Up

앞서 배운 지식과 합쳐서, AOP를 Spring에서 어떻게 사용하는지에 대해 알아볼 수 있었다.