AOP가 어떤식으로 구현되는지는 이해를 했습니다.
이제 그 AOP를 내가 하는게 아니라 스프링 컨테이너가 해줘야 할텐데요
applicationContext 안의 namespace를 보면 AOP를 체크해 줄 수가 있습니다.
이전에는 context를 체크해줘서 component-scan을 해주었던게 기억이 나실겁니다.
이번엔 AOP를 체크해주어 자동으로 구현해보도록 하겠습니다.
일단 Aspect를 하나 만들어봅니다.
package com.duljji;
import org.springframework.stereotype.Component;
@Component
public class TestAspect {
public void before() {
System.out.println("음료수 뚜껑을 연다");
}
public void after() {
System.out.println("음료수 뚜껑을 닫는다.");
}
}
이렇게 aspect를 등록해준뒤, applicationContext에 들어가 AOP를 사용할 준비를 해줍니다.
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<context:component-scan base-package="com.duljji"></context:component-scan>
<aop:config>
<aop:pointcut expression="execution(public void com.duljji.*.greet())" id="pointcut"/>
<aop:aspect ref="testAspect">
<aop:before method="before" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
namespace에서 aop를 체크하여 xmlns:aop가 자동으로 등록되게 한 뒤
aop config를 이용하여 aop를 등록해줍니다.
여기서 AOP의 용어에 대해 잠깐 체크하고 넘어가겠습니다.
- 애스펙트 (Aspect): 애스펙트는 AOP의 기본 단위로, 관심사를 모듈화한 것을 말합니다. 애스펙트는 어드바이스(Advice)와 포인트컷(Pointcut)의 조합으로 구성됩니다. 어드바이스는 특정 지점에서 실행되는 동작을 정의하고, 포인트컷은 어드바이스가 적용될 조인포인트(Joinpoint)를 선택하는 기준입니다.
- 조인포인트 (Joinpoint): 조인포인트는 AOP에서 어드바이스를 적용할 수 있는 프로그램의 실행 지점을 의미합니다. 예를 들어, 메서드 호출, 필드 접근, 예외 발생 등이 조인포인트의 예시입니다. 어드바이스는 조인포인트에서 실행됩니다.
- 포인트컷 (Pointcut): 포인트컷은 애스펙트가 어드바이스를 적용할 조인포인트를 선택하기 위한 기준을 정의하는 것입니다. 포인트컷은 표현식이나 패턴을 사용하여 특정 조인포인트를 지정할 수 있습니다. 예를 들어, "모든 메서드 호출" 또는 "특정 패키지의 모든 클래스의 특정 메서드"와 같은 조건을 포인트컷으로 지정할 수 있습니다.
- 어드바이스 (Advice): 어드바이스는 애스펙트가 조인포인트에서 실행하는 동작을 정의하는 것입니다. 예를 들어, 메서드 호출 전에 실행되는 "Before" 어드바이스, 메서드 실행 후에 실행되는 "After" 어드바이스, 예외가 발생했을 때 실행되는 "AfterThrowing" 어드바이스 등이 있습니다.
- 위빙 (Weaving): 위빙은 애스펙트를 대상 코드에 적용하는 과정을 의미합니다. 즉, 애스펙트의 어드바이스를 포인트컷에 맞는 조인포인트에 삽입하는 것입니다. 위빙은 컴파일 시점, 로드 시점, 런타임 시점에 수행될 수 있습니다. 애스펙트를 적용하는 방법에는 컴파일러, 로더, 프록시 객체 등을 사용하여 위빙을 수행할 수 있습니다.
Aspect를 어느 Joinpoint에서 실행할 지를 정해주는 것이 Poincut입니다.
즉, pointcut부분을 보면 public void com.duljji.*.greet() 이라고 되어있는데 duljji패키지의 모든 클래스가 greet을 실행할 때 동작을 하게 됩니다.
그리고 그 포인트컷의 before가 joinpoint가 되겠네요.
그리고 aop:before는 조인포인트 method="before"는 TestAspect 안에 들어가있는 메서드의 이름입니다.
그럼 이제 스프링이 알아서 weaving을 해주겠군요.
AOP에서 쓰이는 용어도 알아보았습니다.
aop:aspect 에는 ref로 해당 빈의 id값이 들어갈 것이고,
aop:before에는 당연히 어떤 포인트컷의 조인포인트인지 알아야하니 pointcut-ref가 들어가야겠죠?
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<context:component-scan base-package="com.duljji"></context:component-scan>
<aop:config>
<aop:pointcut expression="execution(public void com.duljji.*.greet())" id="pointcut"/>
<aop:aspect ref="testAspect">
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
그럼 이렇게 만들어 준뒤 실행을 하면 어떻게 될까요?
음료수 뚜껑을 연다 -> 인사 -> 음료수 뚜껑을 닫는다 가 실행될것입니다.
아까처럼 proxy패턴을 만들지 않고 그냥 Person객체를 만들어서 실행을 시켰는데도, 자동으로 함수 실행 이전과 이후에 특정 작업을 해주는 것을 볼 수 있습니다.
xml에 직접 넣어주지 않고 간단하게 어노테이션으로도 aspect를 작성할 수가 있습니다.
package com.duljji;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TestAspect {
public void before() {
System.out.println("음료수 뚜껑을 연다");
}
public void after() {
System.out.println("음료수 뚜껑을 닫는다.");
}
}
@Aspect를 붙여주어 Aspect를 만들었습니다.
이 aspect는 파이썬의 decorator와 아주 흡사하게 작동을 합니다.
wrapper메서드를 만들고 함수를 받아서 그 이전, 이후에 작업을 해주는거죠.
wrapper메서드를 하나 만들어주도록 하겟습니다.
package com.duljji;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TestAspect {
@Pointcut("execution(public * com.duljji.*.greet())")
public void wrapper() {}
@Before("wrapper()")
public void before() {
System.out.println("음료수 뚜껑을 연다");
}
@After("wrapper()")
public void after() {
System.out.println("음료수 뚜껑을 닫는다.");
}
}
이렇게 wrapper 메서드가 Pointcut이 되는것이죠.
이제 어노테이션이 동작하게끔 하기 위해서 xml에 당므과 같이 autoproxy를 추가합니다.
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<aop:aspectj-autoproxy />
<context:component-scan base-package="com.duljji"></context:component-scan>
</beans>
그럼 이제 어노테이션으로도 간단하게 AOP를 작성할 수 있게 되었습니다.
그럼 추가적으로 AOP의 다른 Joinpoint도 사용해보도록 하겠습니다.
package com.duljji;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TestAspect {
@Pointcut("execution(public * com.duljji.*.greet())")
public void wrapper() {}
@Before("wrapper()")
public void before() {
System.out.println("음료수 뚜껑을 연다");
}
@After("wrapper()")
public void after() {
System.out.println("음료수 뚜껑을 닫는다.");
}
@AfterReturning(value="wrapper()", returning="num")
public void afterRunning(boolean num) {
if(num == false) {
System.out.println("음료수 병을 버립니다.");
}else {
System.out.println("음료수를 가방안에 넣습니다.");
}
}
}
이번엔 greet을 한 뒤 return값을 만들어주고 num 이라는 변수로 받았습니다.
package com.duljji;
import java.util.Random;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Person {
@Autowired
public Beverage beverage;
public Person() {
}
public Person(Beverage beverage) {
this.beverage = beverage;
}
public boolean greet() {
System.out.println("안녕하세요!! Person입니다.");
return new Random().nextBoolean();
}
}
Person객체의 greet에는 저렇게 boolean값을 리턴해주고, 그 값을 num으로 자동으로 넘겨받는겁니다.
하지만 순서를 보니 AfterReturning 이후에 After 함수가 실행이 되는것을 볼 수 있습니다.
순서를 잘 체크해서 returning에는 return값에 따라 바뀔 함수를 넣고 after함수에는 모든것이 마무리 된 이후에 작업할 것을 넣어야 할 것 같습니다.
AfterThrowing도 써보니 after의 역할을 좀 더 알 것 같습니다.
greet메서드를 다음과 같이 수정하여 간헐적으로 에러가 발생하게 만듭니다.
package com.duljji;
import java.util.Random;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Person {
@Autowired
public Beverage beverage;
public Person() {
}
public Person(Beverage beverage) {
this.beverage = beverage;
}
public boolean greet() {
System.out.println("안녕하세요!! Person입니다.");
if((int)(Math.random() * 100) > 50 ) {
throw new RuntimeException();
}
return new Random().nextBoolean();
}
}
그리고 AfterThrowing 어노테이션을 달아 에러가 발생할 때 호출할 메서드도 만들어줍시다.
package com.duljji;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TestAspect {
@Pointcut("execution(public * com.duljji.*.greet())")
public void wrapper() {}
@Before("wrapper()")
public void before() {
System.out.println("음료수 뚜껑을 연다");
}
@After("wrapper()")
public void after() {
System.out.println("음료수 뚜껑을 닫는다.");
}
@AfterReturning(value="wrapper()", returning="num")
public void afterRunning(boolean num) {
if(num == false) {
System.out.println("음료수 병을 버립니다.");
}else {
System.out.println("음료수를 가방안에 넣습니다.");
}
}
@AfterThrowing(value="wrapper()", throwing="error")
public void afterThrowing(Throwable error) {
System.out.println("음료수를 바닥에 떨어뜨렸습니다!");
}
}
이렇게 하면 에러가 간헐적으로 발생하고 에러가 발생할 경우 음료수를 바닥에 떨어뜨렸습니다!! 라는 문구가 출력이 되겠죠.
음료수를 바닥에 떨어뜨리는 에러가 났음에도 After 메서드는 정상적으로 작동하는 것을 알 수 있습니다.
After보다는 AfterReturning을 더 자주 쓰게 될 것 같은 느낌이 강하게 듭니다.
이렇게 Aspect를 만들어서 사용을 해보았습니다.
이렇게 Logging이나 Security, 트랙잭션 관리와 같은 공통 관심사항들이 이렇게 관점들로 만들어진다면 아주 간단하게 재사용하면서 효율을 올릴 수 있을 것 같습니다.
'백엔드 > Spring' 카테고리의 다른 글
스프링 일기 6 - Spring Legacy Project 만들어보기 (0) | 2023.06.13 |
---|---|
스프링 일기 5 - Web MVC패턴 만들어보기 (0) | 2023.06.13 |
스프링 일기 3 - AOP vs OOP (0) | 2023.06.13 |
스프링 일기 2 - Bean자동으로 생성하게 만들기 (0) | 2023.06.12 |
스프링 일기 1 - Maven Project에 스프링 컨테이너 만들어서 사용해보기 (0) | 2023.06.12 |