저번에 포스팅 했던 Spring의 Transaction의 내용중에 @Transactional 애너테이션이 AOP를 활용하여 만들어지고 활용된다고 해서, AOP가 무엇인지 어느정도는 알고 있지만 항상 AOP를 사용해 본적이 없기에 이것을 사용해서 얻는 이익이 무엇인지 궁금할때가 많았다.
그래서 오늘은 AOP에 대해서 이론을 정리하고, AOP를 사용해보려고 한다.
💡 핵심적인 관점은 우리가 적용하고자 하는 핵심 비즈니스 로직, 부가적인 관점은 핵심 로직 을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
💡 위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-app</artifactId>
</dependency>
implementation 'org.springframework.boot:spring-boot-starter-aop'
애너테이션 이름 | 역할 |
---|---|
@Aspect | AOP로 정의하는 클래스를 지정한다. |
@PointCut | AOP기능을 메서드, Annotation 등 어디에 적용시킬지 지점을 설정한다. 지점을 설정하기 위한 수식들이 매우 많다. |
@Before | 메서드를 실행하기 이전에 실행한다. |
@After | 메서드가 성공적으로 실행후에 실행한다(예외 발생 되더라도 실행 됨) |
@AfterReturing | 메서드가 정상적으로 종료될때 실행된다. |
@AfterThrowing | 메서드에서 예외가 발생할때 실행된다. |
@Around | Before + After 모두 제어한다(예외 발생 되더라도 실행 됨) |
요청에 의해 어떤 Controller 내부의 메서드가 실행되고, 이 메서드가 만약 예외나 문제를 일으킨다면 로그로 남겨서 추적할 필요성이 있다고 볼 수 있다.
그렇다고 해서 매번 Controller 안에서 로깅을 한다면 굉장히 코드가 복잡해지고 책임이 분리되지 못한 코드라고 볼 수 있다.
따라서, Controller에서 메서드의 이름과 매개변수를 출력하지 않고 AOP를 활용해 메서드의 시작 전이나 시작 후, 에러가 발생했을때 등 다양한 상황에서 알맞은 코드가 실행되도록 작성하였다.
// Controller 코드
@RestController
public class AopController {
@CutomAnnotation
@GetMapping("/test/aop/get/{id}")
public String testGetAop(@PathVariable Long id, @RequestParam String pwd) {
return id + " " + pwd;
}
}
// @CustomAnnotation 코드
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CutomAnnotation {
}
// Aspect 클래스 작성
@Aspect
@Component
public class AopLesson {
// com/example/practice/demo/controller 패키지의 하위 클래스들을 모두 적용
@Pointcut("execution(* com.example.practice.demo.controller..*.*(..))")
public void start() {}
// start() 메서드 시작전에 prepare() 메서드 실행
@Before("start()")
public void prepare(JoinPoint joinPoint) {
System.out.println("=========== @Before -> prepare Start ============");
// 실행되는 메서드 이름을 가져오고 출력
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName() + "메서드가 실행중입니다.");
// 메서드에 들어가는 매개변수 배열을 읽어온다.
Object[] paramArgs = joinPoint.getArgs();
// 매개변수 배열의 종류와 값을 출력한다.
for(Object object : paramArgs) {
System.out.println("type = " + object.getClass().getSimpleName());
System.out.println("value = " + object);
}
System.out.println("=========== @Before -> prepare End ============");
System.out.println();
}
// start() 메서드가 종료되는 시점에 afterReturn() 메서드가 실행된다.
// @AfterReturning() 애너테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 한다.
@AfterReturning(value = "start()", returning = "obj")
public void afterReturn(JoinPoint joinPoint, Object obj) {
System.out.println("=========== @AfterReturning Start ============");
// 메서드에 들어가는 매개변수 배열을 읽어온다.
Object[] paramArgs = joinPoint.getArgs();
// 매개변수 배열의 종류와 값을 출력한다.
for(Object object : paramArgs) {
System.out.println("type = " + object.getClass().getSimpleName());
System.out.println("value = " + object);
}
System.out.println("메서드 종료");
System.out.println("=========== @AfterReturning END ============");
System.out.println();
}
@Before("@annotation(com.example.practice.demo.annotation.CutomAnnotation)")
public void annoPrepared() {
System.out.println("=========== @Before -> annoPrepared Start ============");
System.out.println("@CustomAnnotation 붙었다!");
System.out.println("=========== @Before -> annoPrepared End ============");
System.out.println();
}
}
위의 코드가 각각 어떤 동작을 하는지 아래에서 자세히 설명하겠다.
@Pointcut("execution( com.example.practice.demo.controller...*(..))")
@Before("start()")
@AfterReturning(value = "start()", returning = "obj")
@Before("@annotation(com.example.practice.demo.annotation.CutomAnnotation)")
💡 AOP를 사용하니, Controller에는 출력에 관한 코드를 작성하지 않고도 AOP를 사용해 메서드 사용 전후로 다양한 상황에서 부가 기능을 추가할 수 있게된다.