스프링 AOP 구현

slee2·2022년 3월 26일
0

프로젝트 생성

implementation 'org.springframework.boot:spring-boot-starter-aop

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

위 내용을 추가해준다.

예제 프로젝트 만들기

OrderRepository

OrderService

Test

스프링 AOP 구현

1 - 시작

AspectV1

@Around 어노테이션의 값은 포인트컷이 된다.
@Around 어노테이션의 메서드인 doLog()는 어드바이스(Adivce)가 된다.
execution(* hello.aop.order..*(..))hello.aop.order 패키지와 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다.

이렇게 하면 패키지 안에 있는 OrderRepositoyOrderService의 모든 메서드는 프록시가 적용된다.

테스트에 Import어노테이션을 통해 @Aspect가 적용된 클래스를 빈에 넣어주면, 전 후 결과는 아래와 같다.

@Import 전

//success()
2022-03-26 11:21:37.038  INFO 98582 --- [    Test worker] hello.aop.order.OrderService             : [orderService] 실행
2022-03-26 11:21:37.039  INFO 98582 --- [    Test worker] hello.aop.order.OrderRepository          : [orderRepository] 실행

//aopInfo()
2022-03-26 11:21:37.072  INFO 98582 --- [    Test worker] hello.aop.AopTest                        : isAopProxy, orderService=false
2022-03-26 11:21:37.074  INFO 98582 --- [    Test worker] hello.aop.AopTest                        : isAopProxy, orderService=false

//exception()
2022-03-26 11:21:37.107  INFO 98582 --- [    Test worker] hello.aop.order.OrderService             : [orderService] 실행
2022-03-26 11:21:37.107  INFO 98582 --- [    Test worker] hello.aop.order.OrderRepository          : [orderRepository] 실행

@Import 후

//success()
2022-03-26 11:24:23.680  INFO 98792 --- [    Test worker] hello.aop.order.aop.AspectV1             : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.708  INFO 98792 --- [    Test worker] hello.aop.order.OrderService             : [orderService] 실행
2022-03-26 11:24:23.709  INFO 98792 --- [    Test worker] hello.aop.order.aop.AspectV1             : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.731  INFO 98792 --- [    Test worker] hello.aop.order.OrderRepository          : [orderRepository] 실행

//aopInfo()
2022-03-26 11:24:23.768  INFO 98792 --- [    Test worker] hello.aop.AopTest                        : isAopProxy, orderService=true
2022-03-26 11:24:23.768  INFO 98792 --- [    Test worker] hello.aop.AopTest                        : isAopProxy, orderService=true

//exception()
2022-03-26 11:24:23.796  INFO 98792 --- [    Test worker] hello.aop.order.aop.AspectV1             : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.797  INFO 98792 --- [    Test worker] hello.aop.order.OrderService             : [orderService] 실행
2022-03-26 11:24:23.798  INFO 98792 --- [    Test worker] hello.aop.order.aop.AspectV1             : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.798  INFO 98792 --- [    Test worker] hello.aop.order.OrderRepository          : [orderRepository] 실행

2 - 포인트컷 분리

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

AspectV2

@Pointcut

  • 메서드의 반환 타입은 void 여야 한다.
  • 코드 내용은 비워둔다.
  • 포인트컷 시그니처는 allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
  • @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.
  • private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public을 사용해야 한다.

테스트 부분은 동일하기 때문에 생략

3 - 어드바이스 추가

AspectV3

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;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
@Slf4j
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.aop.order 패키지와 하위 패키지
allService() 포인트컷 - 타입 이름 패턴이 *Service.

  • XxxService처럼 Service로 끝나는 것을 대상.
  • 클래스와 인터페이스에 모두 적용됨.

@Around("allOrder() && allService()")

  • 포인트컷은 &&, ||, ! 3가지 조합이 가능하다.
  • 결과적으로 doTransaction() 어드바이스는 OrderService에만 적용된다.

테스트 결과

success()

순서가 doLog -> doTx가 된다.
그런데 반대로 하고 싶다면, 어떻게 해야할까?

는 뒤에서 알아보자.

4 - 포인트컷 참조

이번에는 포인트컷을 외부에서 호출하도록 만들어보자.

Pointcut

AspectV4Pointcut

이런식으로 외부 참조도 가능하다.

5 - 어드바이스 순서

순서를 바꾸기 위해서 @Order를 사용해도 되지만, 이 어노테이션의 경우 @Aspect 단위로만 순서를 바꿔주기 때문에 @Aspect 내부 메서드들의 순서를 바꿀수는 없다.

@Order를 이용해서 순서를 바꾸려면 아래와 같이 하면 된다.

각 클래스를 만들고 @Aspect를 적용한 뒤 @Order를 통해 순서를 정해준다.

어쩔수 없이 클래스를 분리해야한다.

6 - 어드바이스 종류

어드바이스 종류
@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

AspectV6Advice

package hello.aop.order.aop;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@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] {} 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만 사용해도 필요한 기능을 모두 수행할 수 있다.

ProceedingJoinPointorg.aspectj.lang.JoinPoint의 하위 타입이다.

JoinPoint 인터페이스의 주요기능

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

ProceddingJoinPoin의 추가기능

  • proceed(): 다음 어드바이스나 타겟을 호출한다.

success() 테스트

ex() 테스트

@Before

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

@AfterReturning

returning과 파라미터로 받는 Object의 이름과 같아야함.
Object라고 두는 이유는 어떤 반환타입이 들어올지 모르기 때문,
String타입으로 둘 경우, 반환타입이 Integer면, doReturn 메서드 호출이 안됨.

반환 객체를 변경하려면 @Around를 사용해야 한다.

@AfterThrowing

throwing = Exception

@After

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

@Around

  • 메서드 실행 전후 작업을 수행
  • 가장 강력한 어드바이스
    • proceed()의 실행 여부를 결정할 수 있음
    • 전달 값 변환 가능(joinPoint.proceed(args[]))
    • 반환 값 변환
    • 예외 변환
    • 트랜잭션처럼 try - catch - finally 구문 처리 가능
  • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 한다.
  • proceed()를 여러번 실행할 수도 있다.

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

@Around는 항상 joinPoint.proceed()를 호출해야 한다.
호출하지 않으면 타겟이 호출되지 않는다.

@BeforejoinPoint.proceed()를 호출하는 고민을 하지 않아도 된다.
알아서 해주기 때문

그리고 @Before의 경우 바로 코드실행전에 호출되는 것이라는 것을 알 수 있음

좋은 설계는 제약이 있는 것이다.
제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 한다. 만약 @Around 를 사용했는데, 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면? 큰 장애가 발생했을 것이다. 처음부터 @Before 를 사용했다면 이런 문제 자체가 발생하지 않는다.
제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.

0개의 댓글