Advance I/Spring

22.10.20

kinggora 2022. 10. 20. 17:54

스프링 AOP 구현4 - 포인트컷 참조

포인트컷을 공용으로 사용하기 위해 외부 클래스에 모아두기

외부에서 호출하기 위해 포인트컷의 접근 제어자는 public 으로 해두어야 한다.

Pointcuts 클래스

public class Pointcuts {

    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){} //pointcut signature

    //클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    public void allService(){}

    //allOrder() && allService()
    @Pointcut("allOrder() && allService()")
    public void orderAndService(){}
}

 

orderAndService() : allOrder() 포인트컷와 allService() 포인트컷을 조합해서 새로운 포인트컷을 만들었다.

접근 제어자 -> public

AspectV4Pointcut 클래스

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    //hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
    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());
        }
    }
}

 

외부 포인트컷을 사용하는 방법

패키지명을 포함한 클래스 이름과 포인트컷 시그니처(메서드 이름)를 @Around 값에 모두 지정

포인트컷을 여러 어드바이스에서 함께 사용할 때 이 방법을 사용하면 효과적이다.

스프링 AOP 구현5 - 어드바이스 순서

어드바이스는 기본적으로 순서를 보장하지 않는다.

순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다. 문제는 이것을 어드바이스(메서드) 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.

 

로그를 doTransaction() -> doLog() 순으로 남겨보자.

 

@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("[log] {}", joinPoint.getSignature()); //join point 시그니처
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
        //hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
        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());
            }
        }
    }
}

 

하나의 애스펙트 안에 있던 어드바이스를 LogAspect , TxAspect 애스펙트로 각각 분리했다.

그리고 각 애스펙트에 @Order 애노테이션을 통해 실행 순서를 적용했다. 

@Order: @Aspect + 클래스 단위에 적용한다. 숫자가 작을 수록 먼저 실행된다.

 

호출: doTx() -> doLog()

반환: doLog() -> doTx()

 

Aspect 스프링 빈 등록

@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})

스프링 AOP 구현6 - 어드바이스 종류

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

다른 어드바이스는 @Around 의 부분적 기능을 수행한다. 즉, @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.

AspectV6Advice 클래스

@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] {} result={}", 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, ex.getMessage());
    }

    @After("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint){
        log.info("[after] {}", joinPoint.getSignature());
    }
}

 

참고 정보 획득

모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략 가능)

단, @Around 는 ProceedingJoinPoint 을 사용해야 한다.

JoinPoint 와 ProceedingJoinPoint

JoinPoint 인터페이스의 주요 기능

  • getArgs() : 메서드 인수를 반환
  • getThis() : 프록시 객체를 반환
  • getTarget() : 대상 객체를 반환
  • getSignature() : 조언되는 메서드에 대한 설명을 반환
  • toString() : 조언되는 방법에 대한 유용한 설명을 인쇄

 

ProceedingJoinPoint

org.aspectj.lang.JoinPoint 의 하위 타입. 주요 기능으로 다음 어드바이스나 타켓을 호출하는 proceed() 가 있다. 

 

어드바이스 종류

@Before

조인 포인트 실행 전

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

@AfterReturning

메서드 실행이 정상적으로 반환될 때 실행

  • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
  • 타겟 메서드 반환 타입이 returning 절에 지정된 타입과 다를 경우 @AfterReturning 어드바이스 자체가 실행되지 않는다. (부모 타입을 지정하면 모든 자식 타입은 인정된다. void 도 Object는 받을 수 있다.)
  • @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다. (ex. 객체의 setter를 통해)

@AfterThrowing

메서드 실행이 예외를 던져서 종료될 때 실행

  • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
  • @AfterReturning 처럼 throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)

@After

메서드 실행이 종료될 때 실행 (finally)

  • 정상 및 예외 반환 조건을 모두 처리한다.
  • 일반적으로 리소스를 해제하는 데 사용한다.

@Around

메서드 실행의 주변에서 실행 (메서드 실행 전후)

  • 조인 포인트 실행 여부 선택 : joinPoint.proceed() 호출 여부 선택
  • 전달 값 변환: joinPoint.proceed(args[])
  • 반환 값 변환
  • 예외 변환
  • 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
  • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다. (규칙)
  • proceed() 를 여러번 실행할 수도 있음 (재시도)

 

어느바이스 실행 순서

 

실행 순서

스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.

  • @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
  • 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대이다.
  • 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 @Aspect 를 분리하고 @Order 를 적용하자.

@Around 외에 다른 어드바이스가 존재하는 이유

@Before 는 joinPoint.proceed() 를 호출하는 고민을 하지 않아도 된다.

@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.

반면에 @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.

그리고 코드를 작성한 의도가 명확하게 드러난다. @Before 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤 일을 하는 코드구나 라는 것을 알 수 있다.

 

좋은 설계의 조건: 제약

제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 한다. 제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.

11. 스프링 AOP - 포인트컷

포인트컷 지시자

AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다. -> AspectJ pointcut expression

예) @Pointcut("execution(* hello.aop.order..*(..))")

포인트컷 지시자

포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.

  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.

execution 은 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. 따라서 execution 을 중점적으로 이해하자.

예제 만들기

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}

ClassAop: 클래스 단위 애노테이션

 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}

MethodAop: 메서드 단위 애노테이션

 

public interface MemberService {
    String hello(String param);
}

 

@ClassAop
@Component
public class MemberServiceImpl implements MemberService{

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }

    public String internal(String param) {
        return "ok";
    }
}

 

예제 코드 테스트

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    @Test
    void printMethod(){
        //public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
        //execution 으로 시작하는 포인트컷 표현식은 이 메서드 정보를 매칭해서 포인트컷 대상을 찾아낸다.
        log.info("helloMethod={}", helloMethod);
    }
}

 

  • getMethod( 메서드 이름, 파라미터 타입 ) 을 통해 메서드 메타 정보를 받아온다. execution 으로 시작하는 포인트컷 표현식은 이 메서드 정보를 매칭해서 포인트컷 대상을 탐색한다.
  • AspectJExpressionPointcut : 포인트컷 표현식을 처리해주는 클래스. 여기에 포인트컷 표현식을 지정한다. AspectJExpressionPointcut 는 상위에 Pointcut 인터페이스를 가진다.
  • printMethod() 테스트는 MemberServiceImpl.hello(String) 메서드의 정보를 출력해준다.