2번째 필수강의 ch3. 18강 요약
AOP는 DI와 함께 Spring의 핵심 개념 중 하나이다.
예를 들어 한 클래스에 메서드가 여럿 있다고 한다. 그런데 이 메서드들에 공통된 코드가 들어간다고 하면, 변경사항이 발생했을 때 번거롭게 일일이 수정해주어야 한다. 이 상황을 피하기 위해 사용하는 것이 AOP이다.
class MyClass {
void aaa(){
System.out.println("[before]{");
System.out.println("aaa() is called");
System.out.println("}[after]");
}
void abb(){
System.out.println("[before]{");
System.out.println("abb() is called");
System.out.println("}[after]");
}
void ccc(){
System.out.println("[before]{");
System.out.println("ccc() is called");
System.out.println("}[after]");
}
}
각 메서드마다 다른 부분은 핵심 기능이고, 중복된 부분은 부가 기능이다. 서로 다른 두 개의 관심사가 섞여있으므로 먼저 메서드들에서 공통된 코드를 분리한다. invoke 메서드에 사용되는 Method는 java.lang.reflect에서 import한다.
class MyAdvice {
void invoke (Method m, Object obj, Object... args) throws Exception{
System.out.println("[before]{");
m.invoke(obj,args);
System.out.println("}[after]");
}
}
-----
class MyClass {
void aaa(){
System.out.println("aaa() is called");
}
void abb(){
System.out.println("abb() is called");
}
void ccc(){
System.out.println("ccc() is called");
}
}
invoke 메서드는 3개의 메서드(aaa, bbb, ccc)를 각각 호출한다.
다음으로 Main 함수를 작성한다.
public class AopMain {
public static void main(String[] args) throws Exception{
MyAdvice myAdvice = new MyAdvice();
// MyClass 객체를 동적으로 생성해 MyAdvice의 invoke로 넘김
Class myClass = Class.forName("com.fastcampus.ch3.aop.MyClass");
Object obj = myClass.getDeclaredConstructor().newInstance();
for(Method m:myClass.getDeclaredMethods()){
myAdvice.invoke(m, obj, null);
}
}
}
- myAdvice 객체를 생성한다.
- myClass 객체를 생성하고, myClass에 정의된 메서드를 배열로 얻어와 반복문을 돌린다.
- myAdvice 객체의 invoke 메서드 안에 myClass에 정의된 메서드의 정보를 넘겨준다.
이렇게 하면, invoke가 실행되고, myClass의 메서드가 실행된 다음 다시 invoke에 남은 내용이 실행된다.
(두 번째 메서드의 이름은 처음에 만들었던 bbb로 나왔다.)
이렇게 추가할 내용을 분리하여 관리하는 것이 AOP이다.
만약 여기서 3개의 메서드가 아니라 2개에만 이 before와 after를 추가하고 싶다면, myAdvice에 패턴을 추가하면 된다. 예를 들어 a로 시작하는 메서드들에만 이 내용을 추가하고 싶다고 한다. 그러면 다음과 같은 코드를 myAdvice에 추가한다.
Pattern p = Pattern.compile("a.*"); //
boolean matches(Method m){
Matcher matcher = p.matcher(m.getName());
return matcher.matches();
}
정규표현식을 사용해 a로 시작하는 메서드를 찾아 실행한다.
void invoke (Method m, Object obj, Object... args) throws Exception{
if (matches(m)){
System.out.println("[before]{");
}
m.invoke(obj,args);
if(matches(m)){
System.out.println("}[after]");
}
}
제대로 a로 시작하는 메서드만 실행된 것을 알 수 있다.
또는 Pattern이 아닌 애너테이션을 사용할 수 있다. 특정 메서드에만 실행되게 만들고 싶다면 해당 메서드에 @Transactional을 붙여주고, pattern과 일치하는지 보는 부분을
if(m.getAnnotation(Transactional.class)!=null)
로 바꿔준다.
Aspect Oriented Programming이란 관점지향 프로그래밍으로 번역된다. 로깅, 트랜잭션 열기와 닫기, 시큐리티 등은 서로 다른 모듈에서도 공통적으로 사용하는 부분이고, 어떤 기능의 처음이나 마지막 또는 둘 다에서 필요한 작업이다. 이렇게 여러 모듈이나 계층에 공통적으로 쓰인다는 의미를 횡단관심사(Cross-cutting concerns) 라고 한다.
더 쉽게 AOP는 부가기능(advice)를 동적으로 추가해주는 기술이라고 생각하면 된다.
동적이라는 말은 우리가 코드를 작성하지 않고, 코드가 실행되는 과정에서 자동으로 추가된다는 뜻이다.
코드를 자동으로 추가하려면, 메서드마다 코드 라인 수가 다르기 때문에 중간에 추가는 불가하고, 맨 앞과 맨 뒤에 추가하게 된다. 이렇게 맨 앞에 추가하는 것을 BeforeAdvice, 맨 뒤에 추가하는 것을 AfterAdvice, 그리고 양쪽에 다 추가하는 것을 AroundAdvice라고 한다.
용어 | 설명 |
---|---|
target | advice가 추가될 객체 |
advice | target에 동적으로 추가될 부가 기능(코드) |
join point | advice가 추가(join)될 대상(메서드) |
pointcut | join point들을 정의한 패턴 |
proxy | target에 advice가 동적으로 추가되어 생성된 객체 |
weaving | target에 advice를 추가해 proxy를 생성하는 것 |
종류 | 애너테이션 | 설명 |
---|---|---|
Around Advice | @Around | 메서드의 시작과 끝에 부가기능 추가 |
Before Advice | @Before | 메서드의 시작에 부가기능 추가 |
After Advice | @After | 메서드의 끝에 부가기능 추가 |
After Returning | @AfterReturning | 예외 발생 x시 부가기능 실행 (=try 블록 끝) |
After Throwing | @AfterThrowing | 예외 발생 시 부가기능 실행 (=catch 블록 끝) |
advice가 추가될 메서드를 지정하기 위한 패턴
"execution((생략가능한 접근제어자) 반환 타입 패키지명.클래스명.메서드명(매개변수 목록))"
예시: @Around("execution(* com.fastcampus.ch3.aop.*.*(..))"
메서드가 반환하는 타입이 있을 경우 Object로 받아야 한다.
여러 Advice가 적용될 수 있으므로 메서드의 호출 결과를 반환해주어야 한다.
Advice가 여러 개 적용되는 경우, @Order 애너테이션을 사용해 순서를 적용할 수 있다.
AspectJ, Spring-AOP, AspectJ Weaver가 필요하다. 이미 등록되어 있을 수 있지만, 혹시 없으면 추가해준다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.1.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
<scope>runtime</scope>
</dependency>
Target 클래스를 만든다. Java Bean 등록도 해준다.
@Component
public class MyMath {
public int add(int a, int b){
return a+b;
}
public int add(int a, int b, int c){
return a+b+c;
}
public int subtract(int a, int b){
return a-b;
}
public int multiply(int a, int b){
return a*b;
}
}
root-context를 복사한 root-context-aop.xml을 사용해 context를 만든다. 우선 xml 파일을 다음과 같이 수정한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.fastcampus.ch3.aop"/>
</beans>
AopMain2에서 ApplicationContext를 만들어 빈으로 MyMath를 생성하면 메서드가 잘 실행된다.
public class AopMain2 {
public static void main(String[] args) {
ApplicationContext ac = new GenericXmlApplicationContext(
"files:src/main/webapp/WEB-INF/spring/root-context-aop.xml");
MyMath mm = (MyMath) ac.getBean("myMath");
System.out.println(mm.add(1,1));
System.out.println(mm.multiply(4,6));
}
}
다음으로 로깅 advice를 만든다.
@Component
@Aspect
public class LoggingAdvice {
@Around("execution(* com.fastcampus.ch3.aop.MyMath.*(..))")
public Object methodCallLog(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("<start> "+pjp.getSignature().getName()+ Arrays.toString(pjp.getArgs()));
Object result = pjp.proceed(); // target의 메서드를 호출
System.out.println("result = ");
System.out.println("end = "+(System.currentTimeMillis()-start)+"ns");
return result;
}
}
- 마찬가지로 Bean 등록을 해주어야 한다.
- AOP 애너테이션 Aspect를 붙여준다.
- 앞뒤로 advice를 붙여주기 때문에 @Around를 사용했다.
- 시작 시 시간과 종료시간을 재고, 종료시간에서 시작시간을 빼 걸린 시간을 구한다.
그랬는데 이런 오류가 발생했다.
Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 12 in XML document from URL [file:src/main/webapp/WEB-INF/spring/root-context-aop.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 12; columnNumber: 26; cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element 'aop:aspectj-autoproxy'.
xml의 schemaLocation에 aop를 추가하지 않아서 발생한 오류였던 것으로 밝혀졌다!
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
이 부분을 추가하면 제대로 실행되는 것을 확인할 수 있다. (위의 xml에 추가해두었다.)