ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 라는 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤일을 하는 코드 라는 것이 들어난다.

     

    * 좋은 설계는 제약이 있는 것이다 *

    - 제약 덕분에 역할이 명확해진다. 코드의 의도를 파악하기 쉽다.

     

     

     

    출처 : 인프런 김영한님의 스프링 핵심원리 - 고급편

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

    댓글

Designed by Tistory.