스프링 AOP 구현
프로젝트 생성

implementation 'org.springframework.boot:spring-boot-starter-aop
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
위 내용을 추가해준다.
예제 프로젝트 만들기
OrderRepository

OrderService

Test

스프링 AOP 구현
1 - 시작
AspectV1

@Around 어노테이션의 값은 포인트컷이 된다.
@Around 어노테이션의 메서드인 doLog()는 어드바이스(Adivce)가 된다.
execution(* hello.aop.order..*(..))는 hello.aop.order 패키지와 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다.
이렇게 하면 패키지 안에 있는 OrderRepositoy와 OrderService의 모든 메서드는 프록시가 적용된다.

테스트에 Import어노테이션을 통해 @Aspect가 적용된 클래스를 빈에 넣어주면, 전 후 결과는 아래와 같다.
@Import 전
//success()
2022-03-26 11:21:37.038 INFO 98582 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:21:37.039 INFO 98582 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
//aopInfo()
2022-03-26 11:21:37.072 INFO 98582 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=false
2022-03-26 11:21:37.074 INFO 98582 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=false
//exception()
2022-03-26 11:21:37.107 INFO 98582 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:21:37.107 INFO 98582 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
@Import 후
//success()
2022-03-26 11:24:23.680 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.708 INFO 98792 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:24:23.709 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.731 INFO 98792 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
//aopInfo()
2022-03-26 11:24:23.768 INFO 98792 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=true
2022-03-26 11:24:23.768 INFO 98792 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=true
//exception()
2022-03-26 11:24:23.796 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.797 INFO 98792 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:24:23.798 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.798 INFO 98792 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
2 - 포인트컷 분리
@Around에 포인트컷 표현식을 직접 넣을 수도 있지만,
@Pointcut 어노테이션을 사용해서 별도로 분리할 수도 있다.
AspectV2

@Pointcut
- 메서드의 반환 타입은
void여야 한다. - 코드 내용은 비워둔다.
- 포인트컷 시그니처는
allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다. @Around어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.private,public같은 접근 제어자는 내부에서만 사용하면private을 사용해도 되지만, 다른 애스팩트에서 참고하려면public을 사용해야 한다.
테스트 부분은 동일하기 때문에 생략
3 - 어드바이스 추가
AspectV3
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
@Slf4j
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed();
}
//hello.aop.order 패키지와 하위 패키지 면서 동시에 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
allOrder() 포이트컷 - hello.aop.order 패키지와 하위 패키지
allService() 포인트컷 - 타입 이름 패턴이 *Service.
XxxService처럼Service로 끝나는 것을 대상.- 클래스와 인터페이스에 모두 적용됨.
@Around("allOrder() && allService()")
- 포인트컷은
&&,||,!3가지 조합이 가능하다. - 결과적으로
doTransaction()어드바이스는OrderService에만 적용된다.
테스트 결과
success()


순서가 doLog -> doTx가 된다.
그런데 반대로 하고 싶다면, 어떻게 해야할까?
는 뒤에서 알아보자.
4 - 포인트컷 참조
이번에는 포인트컷을 외부에서 호출하도록 만들어보자.
Pointcut

AspectV4Pointcut

이런식으로 외부 참조도 가능하다.
5 - 어드바이스 순서
순서를 바꾸기 위해서 @Order를 사용해도 되지만, 이 어노테이션의 경우 @Aspect 단위로만 순서를 바꿔주기 때문에 @Aspect 내부 메서드들의 순서를 바꿀수는 없다.
@Order를 이용해서 순서를 바꾸려면 아래와 같이 하면 된다.

각 클래스를 만들고 @Aspect를 적용한 뒤 @Order를 통해 순서를 정해준다.

어쩔수 없이 클래스를 분리해야한다.
6 - 어드바이스 종류
어드바이스 종류
@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
AspectV6Advice
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
public class AspectV6Advice {
// @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
// public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// try {
// //@Before
// log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
// Object result = joinPoint.proceed();
// //@AfterReturning
// log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
// return result;
// } catch (Exception e) {
// //@AfterThrowing
// log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
// throw e;
// } finally {
// //@After
// log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
// }
// }
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinpoint) {
log.info("[before] {}", joinpoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinpoint, Object result) {
log.info("[return] {} return={}", joinpoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", ex);
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
@Around를 제외한 나머지 어드바이스들은 @Around의 일부만 제공하는 것.
그러므로 @Around만 사용해도 필요한 기능을 모두 수행할 수 있다.
ProceedingJoinPoint는 org.aspectj.lang.JoinPoint의 하위 타입이다.
JoinPoint 인터페이스의 주요기능
getArgs(): 메서드 인수를 반환합니다.getThis(): 프록시 객체를 반환합니다.getTarget(): 대상 객체를 반환합니다.getSignature(): 조언되는 메서드에 대한 설명을 반환합니다.toString(): 조언되는 방법에 대한 유용한 설명을 인쇄합니다.
ProceddingJoinPoin의 추가기능
proceed(): 다음 어드바이스나 타겟을 호출한다.
success() 테스트

ex() 테스트

@Before

@Around 와 다르게 작업 흐름을 변경할 수는 없다.
@Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다. 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning

returning과 파라미터로 받는 Object의 이름과 같아야함.
Object라고 두는 이유는 어떤 반환타입이 들어올지 모르기 때문,
String타입으로 둘 경우, 반환타입이 Integer면, doReturn 메서드 호출이 안됨.
반환 객체를 변경하려면 @Around를 사용해야 한다.
@AfterThrowing

throwing = Exception
@After
- finally
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스를 해제하는데 사용
@Around
- 메서드 실행 전후 작업을 수행
- 가장 강력한 어드바이스
proceed()의 실행 여부를 결정할 수 있음- 전달 값 변환 가능(
joinPoint.proceed(args[])) - 반환 값 변환
- 예외 변환
- 트랜잭션처럼
try - catch - finally구문 처리 가능
- 어드바이스의 첫 번째 파라미터는
ProceedingJoinPoint를 사용해야 한다. proceed()를 여러번 실행할 수도 있다.

@Around 외에 다른 어드바이스가 존재하는 이유
@Around는 항상 joinPoint.proceed()를 호출해야 한다.
호출하지 않으면 타겟이 호출되지 않는다.
@Before는 joinPoint.proceed()를 호출하는 고민을 하지 않아도 된다.
알아서 해주기 때문
그리고 @Before의 경우 바로 코드실행전에 호출되는 것이라는 것을 알 수 있음
좋은 설계는 제약이 있는 것이다.
제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 한다. 만약 @Around 를 사용했는데, 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면? 큰 장애가 발생했을 것이다. 처음부터 @Before 를 사용했다면 이런 문제 자체가 발생하지 않는다.
제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.
Author And Source
이 문제에 관하여(스프링 AOP 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@seungju0000/스프링-AOP-구현저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)