✔️ 구현
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
AspectV1을 만든다.
@Around : 포인트 컷 부분에 해당
* 모든 타입, hello.aop.order 패키지와 그 하위 패키지(..)를 지정 // 자세한건 다음 포스트에서 설명
@Around가 달린 메서드 doLog는 어드바이스가 된다.
✔️ 스프링 빈 등록
@Aspect가 달린 어드바이저는 스프링 빈으로 등록해줘야한다.
방법
1. @Bean을 사용해 직접 등록
2. @Component 컴포넌트 스캔을 사용해 자동 등록
3. Import 주로 설정 파일을 추가할 때 사용 (@Configuration)
@Import가 빈 등록이 되는 이유
스프링 부트가 기본적으로 AOP 관련 설정을 해주기에 @EnableAspectJAutoProxy를 명시하지 않아도 AOP 프록시가 활성화 된다. 이는 스프링 부트의 자동 설정(Auto-Configuration)기능 덕분이다.
참고
스프링은 프록시 방식의 AOP를 사용, @Aspect 는 AspectJ가 제공하는 애노테이션
✔️ 포인트 컷 분리
@Slf4j
@Aspect
public class AspectV2 {
// hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} // pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Pointcut 에 포인트컷 표현식을 사용
메서드 이름 + 파라미터 = 포인트컷 시그니처
메서드의 반환 타입은 void 여야 한다.
코드 내용은 비움
내부에서만 사용시 private, 다른 애스펙트에 참고시 public 사용
포인트컷 시그니처로 포인트 컷을 지정할 수 있다.
✔️ 타입 이름 패턴과 포인트컷 조합
@Slf4j
@Aspect
public class AspectV3 {
//allOrder() 포인트컷 시그니처..
//클래스 이름 패턴이 *Service
@Pointcut("execution (* *..*Service.*(..))")
private void allService() {}
//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("[resource release] {}", joinPoint.getSignature());
}
}
}
패턴적용도 가능하다.
*xx 면 xx로끝나는 대상을 지정한다. 이를 타입 이름 패턴이라 한다. (클래스, 인터페이스에 모두 적용 가능)
또한 포인트컷 조합도 가능하다. &&(AND), ||(OR), !(Not) 3가지 조합이 가능
포인트 컷 모아두기
// 포인트 컷 모아놓고 사용
public class Pointcuts {
// hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){} // pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution (* *..*Service.*(..))")
public void allService() {}
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
이런식으로 포인트컷만 모아두고 다른 곳에서 불러서 사용 가능하다.
포인트 컷도 조합으로 만들 수 있다.
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//...
}
패키지 명을 다 적어줘야한다.
✔️ 어드바이스 순서
어드바이스는 기본적으로 순서 보장 X
순서 지정을 하고 싶다면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야한다.
그런데 이 애노테이션은 클래스 단위로 적용 가능하다. 따라서 애스펙트를 별도의 클래스로 분리 해야한다.
트랜잭션 먼저 - 그다음 로그
@Slf4j
@Aspect
public class AspectV5Order {
@Aspect
@Order(2) // 숫자가 낮을 수록 우선순위
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
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("[resource release] {}", joinPoint.getSignature());
}
}
}
}
✔️ 어드바이스 종류
@Around : 메서드 호출 전후 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택,
반환 값 변환, 예외 변환, try-catch-finall이 모두 들어가는 구문 처리 가능, proceed()를 여러번실행 가능(재시도)
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Slf4j
@Aspect
public class AspectV6Advice {
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@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("[resource release] {}", joinPoint.getSignature());
}
}
// @Before
// 조인 포인트 전에 간단하게만 작성, 그 이후는 알아서 처리해줌(자동 다음 타겟 호출)
// 작업 흐름변경 불가, 예외 발생시 다음코드 호출 X
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
// @AfterReturning
// returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야함
// returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행 (부모 타입을 지정시 모든 자식 타입 인정)
// 리턴 값을 조작할 순 있지만 바꿀 순 없다. (다른 객체 반환 불가)
@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
// throwing 속성에 사용된 이름은 어드바이스 매서드의 매개변수 이름과 일치해야함
// throwing 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행 (부모 타입을 지정시 모든 자식 타입 인정)
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", ex, ex.getMessage());
}
// @Around
// 메서드 종료시 실행
// 정상 및 예외 반환 조건을 모두 처리
// 일반적으로 리소스 해제시 사용
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
✔️ 참고 정보 획득
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용 가능 (생략 가능)
단 @Around 는 ProceedingJoinPoint(JoinPoint 하위 타입) 를 사용해야한다.
JoinPoint 인터페이스의 주요 기능
- getArgs() : 메서드 인수를 반환
- getThis() : 프록시 객체를 반환
- getTarget() : 대상 객체를 반환
- getSignature() : 조언되는 메서드에 대한 설명을 반환
- toString() : 조언되는 방법에 대한 유용한 설명을 인쇄
ProceedingJoinPoint 인터페이스의 주요 기능
- procced() : 다음 어드바이스나 타겟 호출
✔️@Around 외에 다른 어드바이스가 존재하는 이유
내가 이해한 바로는 각각의 역할이 있다고 본다. @Around를 사용시 모든 처리가 가능하지만 간단하게 실행 시 로그만 찍는 것이 필요한 경우는 굳이 @Around를 쓸 필요없지 않는가? 간단하게 @Before를 사용하는 것이 좋다.
괜히 @Around 사용하려고 만들다가 로직을 잘못짜서 에러가 나는 것보다 @Before을 사용해서 로그만 찍고 그 뒷부분은 자동으로 처리하게 하는게 더 좋다.
좋은 설계는 제약이 있는 것이다. (교재)
🔖 학습내용 출처
'Back-End > Spring Advance & Boot' 카테고리의 다른 글
스프링 고급편 - 실전예제 (0) | 2024.11.13 |
---|---|
스프링 고급편 - 포인트컷 (0) | 2024.11.11 |
스프링 고급편 - 스프링 AOP 개념 (0) | 2024.10.04 |
스프링 고급편 - @Aspect AOP (0) | 2024.10.02 |
스프링 고급편 - 빈 후처리기 (1) | 2024.09.27 |