Spring AOP (2) : 구현

YUZE·2024년 11월 29일
0

Auction

목록 보기
3/5
post-thumbnail

이 포스팅은 간단한 예제를 이용해서, Spring AOP를 구현하며 이해하는 것이 목적이다


AOP를 사용하기 위한 설정


JPA 또는 Spring AOP를 이용해 구현한 것을 프로젝트의 dependency에 이미 가지고 있는 경우, 이미 AOP의존성을 사용하고 있기 때문에, dependency를 명시적으로 넣어주지 않아도 된다.

그렇지 않을 경우, build.gradle에 아래 내용을 추가하여 AOP를 사용하겠다고 명시 해야 한다.

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

@Aspect 를 사용하기 위해서는, main함수 위에 아래 내용을 추가해야 한다. 그러나, 스프링 부트를 사용할 경우 자동으로 추가되어 있으니 따로 명시해주지 않아도 된다.

 @EnableAspectJAutoProxy


AOP 구현


Target : AOP 적용 대상

OrderService

orderItem이 실행되면 log가 찍히고, repository에 save한다.

@RequiredArgsConstructor
@Service
@Slf4j
public class OrderService {
    private final OrderRepository orderRepository;

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

OrderRepository

save가 실행되면 Log가 찍히고, 올바를 경우 "ok" 옳지 않을 경우 예외를 반환한다.

@Slf4j
@Repository
public class OrderRepository {
    public String save(String itemId) {
        log.info("[orderRepository] 실행");

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

Aspect : 여러개의 Adviser을 가지고 있는 클래스(모듈)

@Slf4j
@Aspect
public class AspectV1 {
    @Around("execution(* isyoudwn.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 메서드 호출
    }
}

Advice 개발

    @Around("execution(* isyoudwn.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

@Around 애너테이션의 doLog()

  • 어느 시점에? 어떤 기능을
  • 어드바이스(Advice)

("execution(* isyoudwn.aop.order..*(..))")

  • 포인트 컷
  • isyoudwn.aop.order 패키지와 그 하위 패키지(. .)에 존재하는 모든 메서드들이 실행될 때 실행할 것임

스프링 Aop를 사용하기 위해서는, Bean으로 등록해 주어야 한다

1. @Component
2. @Bean
3. @Import(@Configuration)

위 세가지 중, test 코드에 간단하게 import해서 확인할 것이기 때문에 Import를 사용하였지만, Component Scan을 자동으로 하는 Spring Boot의 특성을 살려, Component를 이용하는 것도 좋은 방법이다.


AOP 적용 확인을 할 테스트 코드 작성

@Slf4j
@SpringBootTest
@Import({AspectV6Advice.class})
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
    }

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

    @Test
    void exception() {
        Assertions.assertThatThrownBy(()
                        -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

실행 결과

  • 전체 실행 결과
  • orderService와 orderRepository에 프록시가 적용 됐는지 확인한다. 원래는 false 였던 값이 true로 변경되었다
  • orderService와 orderRepository가 실행되기 전 AOP가 불리는 모습이다


PointCut 분리


지금까지는 Advcie와 PointCut이 결합된 상태였는데, PointCut은 따로 분리할 수 있다.

@Aspect
@Slf4j
public class AspectV2 {

    @Pointcut("execution(* isyoudwn.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(); // 타깃 호출
    }
}
  • 포인트 컷을 분리해서 사용한다면, 의미를 부여할 수 있는 장점이 있음
  • 포인트 컷들만 모아둔다면 프로그램을 모듈화 할 수 있다는 장점이 있음


Aspect안에 Advice를 추가할 수 있다


@Aspect
@Slf4j
public class AspectV3 {

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

    @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());
        }
    }
}
  • OrderService에는 doLog와 doTransaction 두 어드바이스가 모두 적용된다
  • OrderRepository에는 doLog 어드파이스만 적용된다

  • 트랜잭션 커밋
  • 트랜잭션 롤백

AOP 실행 순서는, 직접 지정해주지 않는 이상 순서가 보장되지 않는다. 더 자세한 내용은 'Advice 순서 지정'에서 설명하도록 하겠다.



PointCut을 모듈화 해서 관리


public class PointCuts {

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

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

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

Advice에서는 포인트 컷을 참조를 통해 부르면 된다

@Slf4j
@Aspect
public class AspectV4 {
    @Around("isyoudwn.aop.order.aop.PointCuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

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


Advice 순서 지정


Advice가 실행되는 순서가 보장되어 있지 않다(즉, 한 메서드에 여러 AOP를 지정하면, 어떤 것이 먼저 실행될지는 모른다는 것이다)

따라서 Order 어노테이션을 이용해서 순서를 직접 지정해 주어야 한다.

그러나 Order 순서는 클래스 단위로 보장된다 → 즉, Aspect 단위로 보장된다.

아래와 같이, 하나의 Aspect 안에 여러 Advice가 존재할 경우 메서드에 Order을 작성해도 순서가 보장되지 않는다는 것이다.


@Slf4j
@Aspect
public class AspectV4
    @Around("isyoudwn.aop.order.aop.PointCuts.allOrder()")
    @Order(2)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
    @Around("isyoudwn.aop.order.aop.PointCuts.orderAndService()")
    @Order(1)
    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
public class AspectV5 {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around("isyoudwn.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("isyoudwn.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());
            }
        }
    }
}

  • 트랜잭션의 순서가 1, Log AOP의 순서가 2일 때 결과
  • Log AOP의 순서가 1, 트랜잭션 AOP의 순서가 2일 때
profile
안녕하세요

0개의 댓글

관련 채용 정보