본문 바로가기
Spring

[Spring] AOP(Aspect Oriented Programming) 학습

by jinjin98 2022. 10. 13.

지난 포스팅에서 AOP 의 개념에 대해 알아보았습니다.

이번 포스팅에서는 예제를 통해 스프링 AOP 로 어드바이저를 어떻게 생성하고 포인트컷 설정 방법 등

스프링 AOP 에 대해 자세히 알아보겠습니다.

 

package hello.aop.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {
        log.info("[orderService] 실행");

        orderRepository.save(itemId);
    }
}

 

package hello.aop.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class OrderRepository {
    public String save(String itemId) {

        log.info("[orderRepository] 실행");

        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        return "ok";
    }
}

 

package hello.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class AspectV1{

    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("[log] {}", joinPoint.getSignature());

        return joinPoint.proceed();
    }
}

 

@Slf4j
@SpringBootTest
@Import(AspectV1.class)
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }
}

 

 

OrderService,  OrderRepository 클래스와 어드바이저인 AspectV1 클래스를 생성했습니다.

스프링 AOP 로 어드바이저를 생성할 때는 클래스를 생생한 후, @Aspect 를 붙여줍니다. 

@Aspect 붙여주고 이 클래스를 스프링 빈으로 등록해주면 자동 프록시 생성기이자 빈 후처리기인

AnnotationAwareAspectJAutoProxyCreator 는 @Aspect 가 붙은 빈을 찾아서 어드바이저로 만들어줍니다.

Advisor(어드바이저) 는 부가 기능인 Advise(어드바이스) 와 부가 기능을 어디에 적용시킬지 조건을 지정하는

Pointcut(포인트컷) 이 하나씩 구성되어 있다고 했습니다. 위 코드에서 @Around 어노테이션이 붙은 

doLog() 메서드가 Advise 이고  @Around 어노테이션 값이 Pointcut 조건입니다. 포인트컷 조건에 해당되는 메서드는

doLog() 메서드의 코드 log.info("[log] {}", joinPoint.getSignature()) 가 적용되는 것입니다.

포인트컷 표현식에 대해서는 뒤에 자세히 설명하겠지만 위 코드에서 사용된 "execution(* hello.aop.order..*(..))"  를

잠시 설명드리자면 반환타입, 메서드 이름, 메서드 파라미터 타입과 개수 상관없이  hello.aop.order 패키지와

하위 패키지에 있는 모든 메서드를 의미합니다.

즉, 위에 OrderService,  OrderRepository 클래스를 대상으로 하는 포인트컨 조건입니다.

doLog() 의 파라미터 ProceedingJoinPoint joinPoint 는 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와

어떤 메서드가 호출되었는지 정보가 포함되어 있습니다. joinPoint.proceed()  메서드는 실제 호출 대상( target )을

호출합니다. 즉 핵심 기능을 실행하는 것입니다.  어드바이스를 생성하는데 사용되는 어노테이션은 @Around

이외에도 여러가지가 있으며 모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 

있으며 생략이 가능합니다. 단 @Around 로 어드바이스를 생성할 때는 ProceedingJoinPoint joinPoint 를 사용해야

되며 어드바이스 메서드의 첫 번째 파라미터에 위치해야 합니다. 또한 항상 joinPoint.proceed()  메서드를 호출해야

합니다. joinPoint.proceed()  메서드를  호출하지 않으면 실제 호출 대상을 호출하지 못하고 다른 어드바이저에

정의되어 있는 다음 부가 기능을 호출하지 못하기 때문입니다. ProceedingJoinPoint 인터페이스는 JoinPoint

인터페이스의 자식 인터페이스이며 JoinPoint 인터페이스에서 많이 사용되는 메서드는 다음과 같습니다.

 

proceed() : 다음 어드바이스나 타켓을 호출합니다.

getArgs() : 메서드 인수를 반환합니다.

getThis() : 프록시 객체를 반환합니다.

getTarget() : 대상 객체를 반환합니다.

getSignature() : 부기 기능이 적용되는 핵심 기능의 메서드에 대한 설명을 반환합니다.

(메서드의 반환타입과 패키지 경로부터 시작하는 메서드명)

.toString() : 조언되는 방법에 대한 유용한 설명을 인쇄합니다.

 

간단하게 어드바이스 생성 방법에 대해 알아보았습니다.

@Around 어노테이션을의 상세한 기능과

@Around 이외에 어드바이스를 생성하는 다른 어노테이션을 알아보겠습니다.

 

@Before: 조인 포인트인 핵심 기능 실행 전에 부가 기능을 호출할 때 사용합니다. 작업 흐름을 변경할 수는 없습니다. 

@Around 는 ProceedingJoinPoint.proceed() 를 사용하지만 @Before 는 사용하지 않습니다.

메서드 종료시 자동으로 다음 타켓이 호출됩니다. 물론 예외가 발생하면 다음 코드가 호출되지는 않습니다.

 

@Before("execution(* hello.aop.order..*(..))")
public void doBefore(JoinPoint joinPoint) {
    log.info("[before] {}", joinPoint.getSignature());
}

 

@AfterReturning: 메서드 실행이 정상적으로 반환될 때 실행합니다. 즉 핵심 기능이 정상적으로 실행될 때 호출합니다.

이 때 returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 합니다.

returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행합니다.

(부모 타입을 지정하면 모든 자식 타입은 인정됩니다.)

@Around 와 다르게 반환되는 객체를 변경할 수는 없습니다. 반환 객체를 조작할 수 는 있습니다.

 

@Around("execution(* hello.aop.order..*(..))")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
{
    try {
        log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
        Object result = joinPoint.proceed();

        log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());

        return result;
    } catch (Exception e) {

        log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
        throw e;
    } finally {

        log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
    }
}

 

@AfterReturning(value = "execution(* hello.aop.order..*(..))",returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
    log.info("[return] {} return={}", joinPoint.getSignature(), result);
}

 

@Around 어노테이션으로 생성한 어드바이스는 @AfterReturning 어노테이션으로 생선한 어드바이스보다 먼저

실행하게 됩니다. @Around 어노테이션은 result 라는 변수를 반환하게 되어 

@AfterReturning 어노테이션으로 생성한 어드바이스에서 지정한 returning 값과 같기 때문에

@AfterReturning 어노테이션으로 생선한 어드바이스가 동작합니다.

 

@AfterThrowing: 메서드 실행이 예외를 던져서 종료될 때 실행합니다.

즉 핵심 기능이 실행되다 예외를 발생시키면 호출합니다. throwing 속성에 사용된 이름은 어드바이스 메서드의

파라미터 이름과 일치해야 합니다. throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행합니다.

(부모 타입을 지정하면 모든 자식 타입은 인정됩니다.)

 

@AfterThrowing(value = "execution(* hello.aop.order..*(..))",throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
    log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}

 

 

@After: 메서드 실행이 종료되면 실행합니다. 즉 핵심 기능이 정상적으로 실행하든, 예외가 발생하든 무조건 실행하는

부가 기능입니다. try ~catch 문에서 finally 와 비슷하다고 볼 수 있습니다.

 

@Around: 메서드의 실행 주변에서 실행됩니다.

메서드 실행 전후에 작업을 수행하도록 부가 기능 실행 시점을 선택할 수 있습니다.

가장 강력한 어드바이스이며 조인 포인트 실행 여부인 joinPoint.proceed() 호출 여부 선택할 수 있으며 전달 값,

반환 값, 예외 변환을 시킬 수 있고 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문도 처리가 가능합니다.

 proceed() 를 여러번 실행할 수도 있습니다. @Around 이외에 다른 어노테이션들은 org.aspectj.lang.JoinPoint 

사용하고 생략할 수 있는 반면에, @Around 는 joinPoint.proceed() 메서드를 꼭 호출해야 한다고 했는데요.

이러한 이유를 다른 어노테이션과 관련지어 다시 설명드리자면 다른 어노테이션은 부가 기능이 언제 호출되는지

정해져있지만 @Around 는 부가 기능 실행 시점을 선택해야 하기 때문입니다.

 

사실 @Around 하나만 있어도 모든 기능을 수행할 수 있습니다.

그런데 다른 어노테이션이 존재하는 이유는 무엇일까요?

@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, joinPoint.proceed() 를 호출하지 못하는 실수를 할 가능성이 

있습니다. 반면에 다른 어노테이션을 사용해서 어드바이스를 생성하면 기능은 적지만 실수할 가능성이 낮고 코드가

단순해집니다. 그리고 가장 중요한 점은 코드를 작성한 의도가 명확하게 드러난다는 점입니다.

예를 들면, @Before 같은 어노테이션을 보는 순간 이 코드는 핵심 기능 실행 전에 실행되는 부가 기능이구나

하고 바로 인지가 된다는 것이죠. 어떻게 보면 기능의 제약으로 인해 역할이 명확해진다는걸 알 수 있는 부분입니다.

 

위 어노테이션을 적용해 어드바이스를 생성했을 경우 실행 순서입니다.

@Around , @Before , @After , @AfterReturning , @AfterThrowing

어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대입니다.

실행 순서를 보면 이렇게 생각할 수 있습니다.

만약 같은 어노테이션을 사용해서 어드바이스를 생성하면 순서가 어떻게 될까?

이 때는 @Order 어노테이션을 사용해야 합니다.

 

@Slf4j
public class AspectV5Order{

    @Aspect
    @Order(2)
    public static class LogAspect {
    
        @Around("execution(* *..*Service.*(..))")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {

        @Around("execution(* hello.aop.order..*(..))")
        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());
            }
        }
    }
}

 

@Slf4j
@SpringBootTest
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }
}

 

 

이렇게 같은 어노테이션을 사용해서 어드바이저를 만들고 원하는 순서대로 부가 기능을 적용하고 싶다면

@Aspect 적용 단위로 @Order 애노테이션을 적용해야 합니다. 이 때 단위는 어드바이스 단위가 아니라 클래스 단위로

적용할 수 있습니다. 따라서 현재 코드처럼 에스펙트를 별도의 정적 클래스로 분리했습니다.

 

어드바이스에 대해서 알아보았으니 이번엔 포인트컷에 대해 살펴보겠습니다.

부가 기능 로직 메서드에 @Around 와 다른 어노테이션을 붙여 어드바이스라는걸 명시해주고 어노테이션 값에는

포인트컷 조건을 설정한다고 했습니다. 포인트컷 조건은 포인트컷 표현식을 작성해 설정하고 포인트컷 표현식은

execution 같은 포인트컷 지시자로 시작합니다. 줄여서 PCD 라고 합니다.

 

 포인트컷 지시자의 종류



execution : 메소드 실행 조인 포인트를 매칭합니다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡합니다.

within : 특정 타입 내의 조인 포인트를 매칭합니다.

args : 인자가 주어진 타입의 인스턴스인 조인 포인트입니다.

this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트입니다.

target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트입니다.

@target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트입니다.

@within : 주어진 애노테이션이 있는 타입 내 조인 포인트입니다.

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

@args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트입니다.

bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정합니다.



가장 많이 사용하는 execution 포인트컷 지시자 문법은

execution(접근제어자? 반환타입 선언타입? 메서드이름(파라미터) 예외?) 처럼 설정할 수 있습니다.

이 중에서 ?가 있는 접근제어자와 선언타입 그리고 예외는 생략이 가능하며 *와  같은 패턴을 지정할 수 있습니다.

* 은 어떤 값이 들어와도 된다는 의미입니다.

"execution(* hello.aop.order..*(..))" 에서는 접근제어자는 생략, 반환타입은 모든 타입 허용, 선언타입은 

hello.aop.order 패키지와 그 하위 패키지 모든 타입 허용, 메서드 이름은 모든 메서드 이름 허용, 

파라미터는 파라미터 타입과 파라미터 수 상관없음, 예외는 생략을 의미합니다.

선언타입을 지정해줄 때 끝에 . 과 .. 이 헷갈릴 수 있습니다. 

은 정확하게 해당 위치의 패키지 .. 은 해당 위치의 패키지와 그 하위 패키지도 포함하는 것을 의미합니다.

그리고 클래스나 인터페이스명을 적어주면 해당 클래스의 자식 클래스, 인터페이스의 구현체까지 매칭이 됩니다.

하지만 자식 클래스나 인터페이스 구현체에만 있는 다른 메서드까지는 매칭이 되지 않습니다.

다음은 파라미터를 알아보겠습니다. 파라미터는 타입과 개수를 지정할 수 있는데요.

() 빈 중괄호만 쓰면 파라미터가 없어야 매칭, (Strintg) 은 String 타입 매칭,

(*) 은 정확히 하나의 파라미터이며 모든 타입 허용, (..) 파라미터 개수와 타입 상관없이 모든 파라미터 허용,

(String, *) 은 파라미터 개수는 2개이며 2번째 파라미터는 모든 타입 허용, (String, ..)  은 2번째 파라미터부터는

모든 타입 허용하며 파라미터가 없어도되고 여러 개도 들어올 수 있습니다. 


포인트컷 표현식은 @Around 값으로 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도

있습니다. 표현식을 따로 분리하게 되면 해당 표현식이 필요한 다른 어드바이스에 재사용을 할 수 있는 장점이 있습니다.

 

@Slf4j
@Aspect
public class AspectV2 {

    @Pointcut("execution(* hello.aop.order..*(..))") 
    private void allOrder(){}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());

        return joinPoint.proceed();
    }
}

 

이렇게 @Pointcut 어노테이션의 값에 포인트컷 표현식을 작성하고 @Around 값에는 @Pointcut 어노테이션이 븥은

메서드 이름을 작성해줍니다. 이 떄 @Pointcut 어노테이션이 붙은 메서드 이름과 파라미터를 합쳐서

포인트컷 시그니처(signature)라 합니다. 포인트컷 시그니처의 반환 타입은 void 이여야 하고 메서드 안 코드 내용은

비워둬야 합니다. 또한 포인트컷 표현식은 && (AND), || (OR), ! (NOT) 3가지 조합이 가능합니다.

 

@Slf4j
@Aspect
public class AspectV3 {
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){}

    @Pointcut("execution(* *..*Service.*(..))")
    private void allService(){}

    @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());
        }
    }
}

 

이렇게 @Pointcut 어노테이션을 이용해 2개의 포인트컷 시그니처를 만들고

@Around 어노테이션 값을 조합하여 설정할 수 있습니다.

그런데 이렇게 포인트컷  @Pointcut 어노테이션이 븥은 메서드 접근제한자를 private 으로 하게 되면

메서드가 속해있는 클래스에 안에서만 포인트컷 표현식을 사용할 수 있습니다.

외부에 있는 다른 클래스안에 어드바이스 메서드에도 사용할 수 있게 하려면 접근제한자를 public 으로 해야합니다.

 

public class Pointcuts {

    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){}

    @Pointcut("execution(* *..*Service.*(..))")
    public void allService(){}

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

 

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    @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());
        }
    }
}

 

이렇게 포인트컷을 별도의 클래스에 모아두고 메서드의 접근제한자를 public 으로 열어 어떤 클래스에서든

포인트컷을 공용으로 사용할 수 있게 하였습니다.

 

지금까지 스프링 AOP 에 대해 알아보았는데요. 좋은 코드는 변경이 일어날 때 드러난다고 합니다.

AOP 를 사용해 부가 기능인 공통 로직을 분리하여 개발한다면

나중에 코드의 변경이 필요할 때 시간과 수고를 덜어줄 것입니다.

댓글