본문 바로가기
BackEnd/Spring

Spring AOP - Advice, Pointcut

by 규난 2023. 8. 6.
728x90

이전 포스트에서는 AOP의 개념과 동작 원리에 대해 알아보았습니다.

2023.08.05 - [Spring] - Spring AOP

 

Spring AOP

이번 포스트에서는 Spring의 3대 요소 중 하나인 Spring AOP에 대해서 알아보도록 하겠습니다. AOP(Aspect Oriented Programming) 일단 AOP가 뭔지 알아보기 전에 Asepect가 뭔지에 대해서 간단하게 알아보도록 하

rbsks.tistory.com

이번 포스트에서는 Spring AOP의 Advice와 Pointcut에 대해서 알아보도록 하겠습니다.

 

Advice

어드바이스란 대상 메서드에 제공할 부가 기능 로직을 담은 모듈입니다. 

 

Advice의 종류

@Around

대상 메소드(조인 포인트) 호출 전후에 수행되며 예외 전환, 반환 값 변환, 대상 실행 여부 선택, 전달 값 변환을 할 수 있는 어드바이스입니다.

ProceedingJoinPoint를 매개변수로 받아야 하며 ProceedingJoinPoint의 proceed() 메소드를 통해 대상을 실행할 수 있으며 proceed(Object[] args)를 통해 전달 값을 변환할 수 있습니다.

 

@Around 어드바이스 예제 코드

위에서 언급한 내용처럼 ProceedingJoinPoint의 proceed() 메소드를 통해 대상 메서드의 실행 여부를 선택할 수 있고 대상 메서드 실행 전후에 로깅을 남기는 등 부가 기능을 추가할 수 있습니다. 또한 예외 발생 시 커스텀 한 예외로 전환할 수 있으며 대상 메서드가 성공적으로 수행되면 결괏값을 조작 또는 반환 객체 자체를 변경해서 반환할 수 있는 가장 강력한 어드바이스입니다.

@Around("execution(* aop.test..*(..))")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        // @Before
        log.info("[대상 메서드 실행 전]");
        Object result = joinPoint.proceed();

        // @AfterReturning
        log.info("[대상 메서드 실행 후]");
        return result;
    } catch (Exception e) {
        // @AfterThrowing
        log.info("[대상 메서드 실행 중 예외 발생]");
        throw e;
    } finally {
        // @After
        log.info("[대상 메서드의 성공 여부와 관계 없이 무조건 실행]");
    }
}

 

@Before

대상 메서드 실행 전에 실행되는 어드바이스입니다.

@Around 어드바이스는 ProceedingJoinPoint의 proceed() 메서드를 사용해서 대상 메서드의 호출 여부를 선택할 수 있었지만 @Before 어드바이스는 ProceedingJoinPoint를 사용하지 않고 JointPoint를 사용하기 때문에 대상 메서드의 호출 여부를 선택할 수 없습니다. @Before 애노테이션이 붙은 메서드가 정상적으로 종료되면 자동으로 대상 메서드(다음 타겟)이 호출되게 됩니다.

 

@Before 어드바이스 예제 코드

@Before("execution(* aop.test..*(..))")
public Object doBeforeAdvice(JoinPoint joinPoint) throws Throwable {
    // @Before
    log.info("[대상 메서드 실행 전]");
}

 

@AfterReturning

대상 메서드의 실행이 정상적으로 수행되고 나서 실행되는 어드바이스입니다.

ProceedingJoinPoint를 사용하지 않고 JointPoint를 사용하기 때문에 결괏값의 조작은 가능하나 @Around 어드바이스와 다르게 반환 객체를 변경할 수는 없습니다. @AfterReturning 애노테이션시 사용 시 주의점은 애노테이션의 returning 속성에 사용되는 이름은 어드바이 메서드의 매개변수의 이름과 일치해야 합니다.

 

@AfterReturning 어드바이스 예제 코드

@AfterReturning("execution(* aop.test..*(..))")
public Object doAfterReturningAdvice(JoinPoint joinPoint) throws Throwable {
    // @AfterReturning
    log.info("[대상 메서드 실행 후]");
}

 

@AfterThrowing

대상 메서드 실행이 예외를 던질 시 실행되는 어드바이스입니다.

@AfterThrowing 애노테이션시 사용 시 주의점은 애노테이션의 throwing 속성에 사용되는 이름은 어드바이 메서드의 매개변수의 이름과 일치해야 합니다. 매개변수로 지정된 예외 타입과 맞는 예외만 대상으로 어드바이스가 실행되게 됩니다. 이때 부모 예외 타입으로 지정하면 하위의 모든 자식 예외 타입까지 인정되어 어드바이스가 실행되게 됩니다.

 

@AfterThrowing 어드바이스 예제 코드

@AfterThrowing("execution(* aop.test..*(..))")
public Object doAfterThrowingAdvice(JoinPoint joinPoint) throws Throwable {
    // @AfterThrowing
    log.info("[대상 메서드 실행 중 예외 발생]");
}

 

@After

대상 메서드의 성공 여부와 관계없이 무조건 실행되는 어드바이스입니다. (finally)

 

@After 어드바이스 예제 코드

@After("execution(* aop.test..*(..))")
public Object doAfterAdvice(JoinPoint joinPoint) throws Throwable {
    // @After
    log.info("[대상 메서드의 성공 여부와 관계 없이 무조건 실행]");
}

 

Pointcut

포인트 컷은 어드바이스를 적용할 조인 포인트를 선별하는 작업의 기능을 정의한 모듈입니다.

Spring AOP의 조인 포인트는 메서드 실행 지점이므로 Spring의 Pointcut은 메서드를 선정하는 기능을 가지고 있습니다.

즉, 타겟 객체의 메서드 중 어느 메서드에 어드바이스를 적용할지 판단하는 필터링 로직을 담담하는 곳입니다.

 

Poincut의 역할

빈 후처리기가 스프링 빈으로 등록되어 있으면 스프링 컨테이너는 빈 오브젝트를 만든 후 빈 후처리기에게 빈을 전달하고 빈 후처리기에서 스프링 컨테이너에 빈으로 등록된 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인 후 프록시 적용 대상이면 내장된 프록시 생성기를 이용해 전달받은 빈에 대한 프록시를 만들게 되고 만들어진 프록시에 어드바이저를 연결한 후 다시 스프링 컨테이너에게 전달해 주고 스프링 컨테이너는 전달받은 프록시 객체를 스프링 컨테이너에 빈으로 등록하게 됩니다. 

 

정리하자면 포인트 컷은 타겟 객체의 메소드 중 어느 메소드에 부가 기능을 적용할지 선정해 주는 역할과 스프링 컨테이너에서 전달받은 빈을 빈 후처리기에서 프록시 적용 대상인지 확인해 주는 역할을 하게 됩니다.

 

Pointcut 지시자

Pointcut 지시자의 종류는 많지만 자주 사용되는 지시자는 그렇게 많지 않은 듯싶습니다.

이번 포스트에서는 자주 사용되는 지시자만 포스팅하도록 하겠습니다.

 

execution

execution 문법에 해당하는 메서드에 조인 포인트를 매칭하며 Spring AOP에서 가장 많이 사용됩니다.

execution 문법은 execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?) 이며 ?가 붙은 부분은 생략이 가능하고 '*' 같은 패턴을 사용할 수 있습니다.

 

execution 사용 예제 코드

package hello.aop.pointcut;

import hello.aop.member.MemberServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;

import java.lang.reflect.Method;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
public class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); // pointcut 표현식을 처리해주는 클래스
    Method helloMethod;

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

    @Test
    public void printMethod() {
        // pointcut의 execution에 매칭되는 메서드 정보
        // public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
        log.info("helloMethod={}", helloMethod);
    }

    @Test
    public void exactMatch() {
        // public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
        pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void allMatch() {
        // 접근제어자, 선언타입, 예외 생략
        // 모든 반환타입/메서드이름, 모든 파라미터 타입과 파라미터 수 매칭
        pointcut.setExpression("execution(* *(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void nameMatch() {
        // 접근제어자, 선언타입, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello 메서드 매칭
        pointcut.setExpression("execution(* hello(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void nameMatchPattern1() {
        // 접근제어자, 선언타입, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hel* 메서드 매칭
        pointcut.setExpression("execution(* hel*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void nameMatchPattern2() {
        // 접근제어자, 선언타입, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, *el* 메서드 매칭
        pointcut.setExpression("execution(* *el*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void nameMatchFalse() {
        // 접근제어자, 선언타입, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, nono 메서드 매칭
        pointcut.setExpression("execution(* nono(..))");
        assertFalse(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void packageExactMatch() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.MemberServiceImpl 선언 타입, hello 메서드 매칭
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void packageMatchPattern1() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.* 선언 타입, * 메서드 매칭
        pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void packageMatchPattern2() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.*.* 선언 타입, * 메서드 매칭
        pointcut.setExpression("execution(* hello.aop..*.*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void packageMatchFalse() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.* 선언 타입, * 메서드 매칭
        pointcut.setExpression("execution(* hello.aop.*.*(..))");
        assertFalse(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void typeExactMatch() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.MemberServiceImpl; 선언 타입, * 메서드 매칭
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void typeMatchSuperType() {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.MemberService 선언 타입, * 메서드 매칭
        // 자식 타입은 부모 타입으로 매칭이 된다. 그래서 타입 매칭이라고 부름
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    public void typeMatchInternal() throws Exception {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.MemberServiceImpl 선언 타입, * 메서드 매칭
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
        Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
        assertTrue(pointcut.matches(internalMethod, MemberServiceImpl.class));
    }

    @Test
    public void typeMatchNoSuperTypeMethodFalse() throws Exception {
        // 접근제어자, 예외 생략
        // 모든 반환타입, 모든 파라미터 타입과 파라미터 수, hello.aop.member.MemberService 선언 타입, * 메서드 매칭
        // 자식 타입은 부모 타입으로 매칭이 된다. 그래서 타입 매칭이라고 부름
        // 하지만 부모 타입에 있는 메서드만 매칭이 된다. 그래서 MemberServiceImpl에 있는 internal method는 매칭되지 않는다.
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
        assertFalse(pointcut.matches(internalMethod, MemberServiceImpl.class));
    }

    @Test
    @DisplayName("String type (String)")
    public void argsMatch() {
        pointcut.setExpression("execution(* *(String))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    @DisplayName("no args ()")
    public void argsMatchNoArgs() {
        pointcut.setExpression("execution(* *())");
        assertFalse(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    @DisplayName("정확히 하나의 파라미터 허용, 모든 타입 허용")
    public void argsMatchPattern() {
        pointcut.setExpression("execution(* *(*))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    @DisplayName("파라미터 수와 무관, 모든 타입 허용")
    public void argsMatchAll() {
        pointcut.setExpression("execution(* *(..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }

    @Test
    @DisplayName("String 타입으로 시작하고 파라미터 수와 무관, 모든 타입 허용")
    public void argsMatchComplex() {
        pointcut.setExpression("execution(* *(String, ..))");
        assertTrue(pointcut.matches(helloMethod, MemberServiceImpl.class));
    }
}

 

@annotation

메서드가 주어진 애노테이션을 가지고 있는 경우 조인 포인트를 매칭합니다.

 

@annotation 사용 예제 코드

package hello.aop.pointcut;


import hello.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    private MemberService memberService;

    @Test
    public void success() {
        log.info("memberService proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }


    @Slf4j
    @Aspect
    public static class AtAnnotationAspect {

        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

args

어드바이스 메소드의 매개변수 타입 또는 부모 타입과 같은 경우 조인 포인트를 매칭합니다.

 

args 사용 예제 코드

package hello.aop.pointcut;

import hello.aop.member.MemberService;
import hello.aop.member.annotation.MethodAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

    @Autowired
    private MemberService memberService;

    @Test
    public void success() {
        log.info("memberService proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    public static class ParameterAspect {

        @Pointcut("execution(* hello.aop.member..*.*(..))")
        private void allMember() {}

        // join point의 getArgs로 파라미터를 가져오는 방식
        @Around("allMember()")
        public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
            Object arg = joinPoint.getArgs()[0];
            log.info("[logArgs1 getArgs]{} arg={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        // args를 이용하여 파라미터를 가져오는 방식. 파라미터가 Object 타입과 Object의 하위 타입이 아니면 실행하지 않음
        @Around("allMember() && args(arg, ..)")
        public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
            log.info("[logArgs2 args]{} arg={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        // args를 이용하여 파라미터를 가져오는 방식. 파라미터가 String 타입과 String의 하위 타입이 아니면 실행하지 않음
        @Around("allMember() && args(arg,..)")
        public Object logArgs3(ProceedingJoinPoint joinPoint, String arg) throws Throwable {
            log.info("[logArgs3 args]{} arg={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        // this는 스프링 컨테이너에 등록된 빈의 프록시 객체를 어드바이스에서 전달받을 수 있도록 함.
        @Around("allMember() && this(obj)")
        public Object logArgs4(ProceedingJoinPoint joinPoint, MemberService obj) throws Throwable {
            log.info("[logArgs4 this]{} arg={}", joinPoint.getSignature(), obj.getClass());
            return joinPoint.proceed();
        }

        // target은 프록시 객체가 호출하는 실제 대상 구현체를 어드바이스에서 전달받을 수 있도록 함.
        @Around("allMember() && target(obj)")
        public Object logArgs5(ProceedingJoinPoint joinPoint, MemberService obj) throws Throwable {
            log.info("[logArgs5 target]{} arg={}", joinPoint.getSignature(), obj.getClass());
            return joinPoint.proceed();
        }

        // annotation에 적용한 value를 가져올 수 있다.
        @Around("allMember() && @annotation(obj)")
        public Object logArgs6(ProceedingJoinPoint joinPoint, MethodAop obj) throws Throwable {
            log.info("[logArgs6 @annotation]{} arg={} annotationValue={}", joinPoint.getSignature(), obj.getClass(), obj.value());
            return joinPoint.proceed();
        }
    }
}

 

728x90

'BackEnd > Spring' 카테고리의 다른 글

Spring AOP  (0) 2023.08.05
예외와 예외 처리 방법  (2) 2023.04.09
스프링의 템플릿/콜백 패턴  (0) 2023.04.02
Spring - 테스트와 TDD  (1) 2023.03.26
Spring - 오브젝트와 의존관계(스프링의 IoC와 DI)  (0) 2023.03.21