본문 바로가기
Spring

[Spring] Spring AOP 사용 방법 - @Aspect 활용

by 개미가되고싶은사람 2024. 9. 27.

목차

    모든 예제에서 스프링 컨테이너에 빈을 저정하는 코드는 따로 없습니다.ㅎㅎㅎ!!

     

    기본 예제

    @Aspect
    @Slf4j
    public class AspectV1 {
    
       	@Around("execution(* hello.aop.order..*(..))")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("AspectV1.doLog={}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    포인트컷은 메소드나 클래스에 적용할 부가기능을 결정하는 애노테이션입니다. 이때 메소드 명, JDK 정규 표현식,  aspectJ 표현식 등으로 조건을 표현할 수 있습니다.

     

     

    추가적으로 @Around, @Before, @After 등등 다양한 애노테이션으로 사용할 수 있습니다. 하지만 @Pointcut과 동작 방식이 다르므로 적절한 방법을 선택하여 사용해야 합니다. 그리고 @Around에만 ProceedingJoinPoint만 사용할 수 있고 나머지 @Pointcut 관련 애노테이션들은 JoinPoint를 사용해야 합니다.

     

    포인트 컷 메소드화

    @Aspect
    @Slf4j
    public class AspectV2 {
    
        @Pointcut("execution(* hello.aop.order..*(..))")
        private void allOrder() {}// pointcut signature
    
    
        @Around("allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("AspectV2.doLog={}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

     

    포인트컷을 메소드화 할 때는 사용 용도에 따라 접근 제어자를 public, private를 사용할 수 있습니다.

     

     

    포인트컷 메소드 모듈화

    @Pointcut 관련 메소드를 모듈화하여 다른 클래스에 작성할 수 있습니다.

    @Aspect
    @Slf4j // 포인트 컷 관련 메소드 모듈화
    public class AspectV4Pointcut {
    
        @Around("hello.aop.order.aop.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("AspectV4.doLog={}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    
    }
    
    
    public class Pointcuts {
        @Pointcut("execution(* hello.aop.order..*(..))")
        public void allOrder() {}
    
        @Pointcut("execution(* *..*Service.*(..))")
        public void allService() {
        }
    
        @Pointcut("allOrder() && allService()")
        public void orderAndService() {
        }
    }

    다른 클래스에 있는 @Pointcut 메소드를 사용하려면 패키지 절대 경로 + 메소드 명을 작성하면 됩니다.

     

    어드바이스 순서 변경 - 내부 클래스 생성

    @Aspect
    @Slf4j // 어드바이스 순서 변경
    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("AspectV5.doLog={}", joinPoint.getSignature());
                return joinPoint.proceed();
            }
        }
    
        @Aspect
        @Order(1)
        public static class TxAspect {
            @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 애노테이션을 메소드에 작성하면 원하는 결과를 도출할 수 있지 않을까 라고 생각할 수 있지만 @Order 애노테이션은 클래스 단위로 적용되므로, 메소드에 작성해도 원하는 결과를 얻을 수 없습니다. 순서를 변경하고 싶다면 어드바이스를 클래스화해야 합니다.

    클래스화하는 방법에는 내부 클래스를 사용하는 방법과 별도의 클래스를 생성하는 방법이 있습니다. 클래스를 세분화했기 때문에, 스프링 빈을 등록할 때 세분화한 클래스를 등록해야 합니다.

     

    어드바이스에서 전달 받은 매개변수 꺼내오기

    this, target, args,@target, @within, @annotation, @args를 사용해서 전달 받은 매개변수를 어드바이스에서 확인할 수 있습니다.

            // ProceedingJoinPoint를 이용한 방식
            @Around("allMember()")
            private Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
                Object arg = joinPoint.getArgs()[0];
                log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg);
                return joinPoint.proceed();
            }
    
            // args를 이용한 방식
            @Around("allMember() && args(arg,..)")
            public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
                log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
                return joinPoint.proceed();
            }
    
            // this를 이용한 방식
            @Before("allMember() && this(obj)")
            public void thisArgs(JoinPoint joinPoint, MemberService obj) {
                log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
            }
    
            // target를 이용한 방식
            @Before("allMember() && target(obj)")
            public void targetArgs(JoinPoint joinPoint, MemberService obj) {
                log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
            }
    
            // @target를 이용한 방식
            @Before("allMember() && @target(annotation)")
            public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
                log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
            }
    
            // @within를 이용한 방식
            @Before("allMember() && @within(annotation)")
            public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
                log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
            }
    
            // @annotation를 이용한 방식, annotation에 있는 멤버 변수의 값을 가져옴
            @Before("allMember() && @annotation(annotation)")
            public void atAnnotation(JoinPoint joinPoint, MethodeAop annotation) {
                log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
            }

     

    사용 시 주의사항

    • 포인트컷에서 사용한 이름과 매개변수의 이름이 같아야 합니다.
    • 지시자 종류에 따라서 타입이 메서드에 지정한 타입으로 제한될 수 있고 부모 타입을 사용할 수 있습니다.

     

     

    참고

    [1] - 스프링 핵심 원리 고급편 - 김영한