-
Spring 핵심 원리 고급편 - 10. 스프링 AOP 구현Spring-Boot/스프링핵심원리 - 고급편 2022. 6. 9. 09:03
//테스트에서 lombok 사용 testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok'
- 테스트에서 롬복 사용 시 Gradle 추가
* 참고 *
- @Aspect 를 사용하려면 @EnableAspectJAutoProxy 를 스프링 설정에 추가해야 하지만, 스프링 부트를 사용하면 자동으로 추가된다.
@Slf4j @Aspect public class AspectV1 { @Around("execution(* hello.aop.order..*(..))") public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처 return joinPoint.proceed(); } }
- @Around 애노테이션의 값인 execution(* hello.aop.order..*(..)) 는 포인트컷이 된다.
- @Around 애노테이션의 메서드인 doLog는 어드바이스(Advice) 가 된다.
- execution(* hello.aop.order..*(..)) 는 hello.aop.order 패키지와 그 하위 패키지 ( .. )를 지정하는 AspectJ 포인트컷 표현식이다.
- 참고로 스프링은 프록시 방식의 AOP를 사용하므로 프록시를 통해는 메서드만 적용 대상이 된다.
* 참고 *
- @Aspect를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다.
- 앞서 build.gradle 에 spring-boot-starter-aop를 포함했는데, 이렇게 하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar도 함께 사용할 수 있게 의존 관계에 포함된다.
- 그런데 스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고, 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다. 스프링은 프록시 방식의 AOP를 사용한다.
- @Aspect는 애스팩트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다.
* 스프링 빈으로 등록하는 방법 *
1. @Bean 을 사용해서 직접 등록
2. @Component 컴포넌트 스캔을 사용해서 자동 등록
3. @Import 주로 설정 파일을 추가할 때 사용 @Configuration
ㆍ @Import 는 주로 설정 파일을 추가할 때 사용하지만, 이 기능으로 스프링 빈도 등록할 수 있다.
포인트 컷 분리
- @Around 에 포인트컷 표현식을 직접 넣을 수도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수도 있다.
@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()); // join point 시그니처 return joinPoint.proceed(); } }
* @Pointcut *
- @Pointcut 에 포인트컷 표현식 사용한다
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다
- 메서드의 반환 타입은 void 여야 한다
- 코드 내용은 비워둔다.
- 포인트컷 시그니처는 allOrder( ) 이다.
- @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다
@Around("allOrder( )") 사용
- private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public을 사용해야한다.
- 이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있다.
- 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있다.
어드바이스 추가
- 트랜잭션 기능 적용 코드 추가
트랜잭션 기능
- 핵심 로직 실행 직전에 트랜잭션을 시작
- 핵심 로직 실행
- 핵심 로직 실행에 문제가 없으면 커밋
- 핵심 로직 실행에 예외가 발생하면 롤백
@Slf4j @Aspect 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.app.order ' 패키지와 하위 패키지를 대상으로 한다
- allService( ) 포인트컷은 타입 이름 패턴이 ' *Service ' 를 대상으로 하는데 쉽게 이야기해서 ' XxxService '처럼 ' Service ' 로 끝나는 것을 대상으로 한다. ' *Servi* ' 과 같은 패턴도 가능하다
- 여기서 타입 이름 패턴이락고 한 이유는 클래스,인터페이스에 모두 적용되기 때문이다.
@Around("allOrder() && allService()")
- 포인트컷은 이렇게 조합할 수 있다. &&(AND), ||(OR), !(NOT) 3가지 조합이 가능하다.
- hello.aop.order 패키지와 하위 패키지 이면서 타입 이름 패턴이 *Service 인 것을 대상으로 한다
- 결과적으로 doTransaction( ) 어드바이스는 OrderService 에만 적용된다
포인트컷 참조
- 포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 된다.
- 참고로 외부에서 호출할 때는 포인트컷의 접근제어자를 public으로 열어 두어야 한다.
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() {} } // 사용 방법 @Around("hello.aop.order.aop.Pointcuts.allOrder()")
어드바이스 순서
- 어드바이스는 기본적으로 순서를 보장하지 않다.
- 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용
- 문제는 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점
- 그래서 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다
- 애스펙트를 별도의 클래스로 분리해야한다.
@Slf4j public class AspectV5 { @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()); // join point 시그니처 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("[리소스 릴리즈] {}", joinPoint.getSignature()); } } } }
- 내부 클래스 사용
- 참고로 @Order 숫자가 작을 수록 먼저 실행된다.
어드바이스 종류
* 어드바이스 종류 *
- @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인포인트 실행여부 선택, 반환 값 변환, 예외 변환 등이 가능
- @Before : 조인 포인트 실행 이전에 실행
- @AfterReturning : 조인 포인트 정상 완료 후 실행
- @AfterThrowing : 메서드가 예외를 던지는 경우 실행
- @After : 조인 포인트가 정상 또는 예외에 관계 없이 실행( finally )
@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){ // Before 후 joinPoint.proceed를 알아서 실행 log.info("[before] {}", joinPoint.getSignature()); } @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result") public void doReturn(JoinPoint joinPoint, Object result){ // 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 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.
참고 정보 획득
- 모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. ( 생략해도 된다. )
- 단 @Around는 ProceedingJoinPoint 을 사용해야 한다
- 참고로 ProceedingJoinPoint 는 org.aspectj.lang.JoinPoint 의 하위 타입이다
* JoinPoint 인터페이스의 주요 기능 *
- getArgs( ) : 메서드 인수를 반환합니다
- getThis( ) : 프록시 객체를 반환합니다
- getTarget( ) : 대상 객체를 반환합니다
- getSignature( ) : 조언되는 메서드에 대한 설명을 반환합니다
- toString( ) : 조언되는 방법에 대한 유용한 설명을 인쇄합니다.
* ProceedingJoinPoint 인터페이스의 주요 기능 *
- proceed( ) : 다음 어드바이스나 타켓을 호출한다.
@Before
- 조인 포인트 실행 전
- @Around와 다르게 작업 흐름을 변경할 수 없다
- @Around는 ProceedingJoinPoint.proceed( ) 를 호출해야 다음 대상이 호출된다.
ㆍ만약 호출하지 않으면 다음 대상이 호출되지 않는다
- 반면에 @Before는 ProceedingJoinPoint.proceed( ) 자체를 사용하지 않는다
ㆍ메서드 종료시 자동으로 다음 타켓이 호출된다.
ㆍ물론 예외가 발생하면 다음 코드가 호출되지 않는다
@AfterReturning
- 메서드 실행이 정상적으로 반환될 때 실행
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야한다
- returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다 ( 부모 타입을 지정하면 모든 자식 타입은 인정된다. )
- @Around 와 다르게 반환되는 객체를 변경할 수 없다. 반환 객체를 변경하려면 @Around를 사용해야 한다
- 반환객체를 조작할 수는 있다.
@AfterThrowing
- 메서드 실행이 예외를 던져서 종료될 때 실행
- throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야한다
- throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행한다. ( 부모 타입을 지정하면 모든 자식 타입은 인정된다. )
@After
- 메서드 실행이 종료되면 실행된다. ( finally를 생각하면 된다 )
- 정상 및 예외 반환 조건을 모두 처리한다
- 일반적으로 리소스를 해제하는데 사용한다.
@Around
- 메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다
- 가장 강력한 어드바이스
ㆍ조인포인트 실행 여부 선택 ' joinPoint.proceed( ) 호출 여부 선택 '
ㆍ전달 값 변환 : ' joinPoint.proceed(arg[]) '
ㆍ반환 값 변환
ㆍ예외 변환
ㆍ트랜잭션 처럼 ' try ~ catch ~ finally ' 모두 들어가는 구문 처리 가능
- 어드바이스의 첫 번째 파라미터는 ' ProceedingJoinPoint ' 를 사용해야 한다
- ' proceed( ) ' 를 통해 대상을 실행한다
- ' proceed( ) ' 를 여러번 실행할 수도 있음( 재시도 )
* 순서 *
- 실행 순서 : @Around, @Before, @After, @AfterReturning, @AfterThrowing
- 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자
- 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다
ㆍ이 경우 앞서 배운 것처럼 @Aspect 를 분리하고 @Order 를 적용하자
* @Around 외에 다른 어드바이스가 존재하는 이유 *
- @Around 는 항상 joinPoint.proceed( ) 를 호출해야한다
- 만약 호출하지 않으면 타켓이 호출되지 않는 치명적인 버그가 발생한다.
- @Before 는 joinPoint.proceed( ) 를 호출하는 고민을 하지 않아도 된다
- @Around가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.
- 반면에 @Before, @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드가 단순하다.
- 그리고 가장 중요한 점은 코드를 작성한 의도가 명확하게 들어난다는 점이다
- @Before 라는 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤일을 하는 코드 라는 것이 들어난다.
* 좋은 설계는 제약이 있는 것이다 *
- 제약 덕분에 역할이 명확해진다. 코드의 의도를 파악하기 쉽다.
출처 : 인프런 김영한님의 스프링 핵심원리 - 고급편
'Spring-Boot > 스프링핵심원리 - 고급편' 카테고리의 다른 글
Spring 핵심 원리 고급편 - 13. 스프링 AOP - 실무 주의사항 (0) 2022.06.10 Spring 핵심 원리 고급편 - 11. 스프링 AOP - 포인트컷 (0) 2022.06.09 Spring 핵심 원리 고급편 - 9. 스프링 AOP 개념 (0) 2022.06.08 Spring 핵심 원리 고급편 - 8. @Aspect AOP (0) 2022.06.08 Spring 핵심 원리 고급편 - 7. 빈 후처리기 (0) 2022.06.08