AOP 구현

바그다드·2023년 10월 5일
0

지난 포스팅에서 AOP에 대해 알아보았다.
AOP란 핵심 기능과 부가 기능으로 관점을 나눠 관리하는 것을 말한다.
여기서 AOP를 적용하는 방법은 3가지가 있었다.
컴파일 시점, 클래스 로더 적용 시점, 런타임 시점
우리가 여태했던 방식은 런타임(프록시)시점으로 조인포인트는 메서드 실행 시점으로만 제한되고 다른 제약 사항들도 존재하지만, 별도의 컴파일러나 클래스 로더 조작기 없이 단지 스프링만 있으면 적용할 수 있다는 특징이 있었다.

이번 포스팅에서는 이 세번째 프록시를 이용한 AOP 적용 방법을 이용해 실제 AOP를 구현하는 방법에 대해 알아보자.

의존성 추가

  • 먼저 스프링에서 제공하는 AOP기능을 사용하기 위해 다음을 추가해주자
implementation 'org.springframework.boot:spring-boot-starter-aop'
  • 추가로 테스트 코드에서 lombok을 사용하기 위해 다음 코드도 추가하자
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

예제 생성

1. Repository생성

@Slf4j
@Repository
public class OrderRepository {
    public String save(String itemId) {
        log.info("[orderRepository] 실행");
        //저장 로직
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        return "ok";
    }
}

2. 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);
    }
}

간단하게 구현된 예제이다.

3. Test생성

@Slf4j
@SpringBootTest
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);
    }
}
  • aopInfo()
    리포지토리와 서비스에 프록시가 적용되어 있는지 확인하는 메서드로 현재는 당연히 프록시가 적용되지 않은 상태이다.

    이제 여기에 AOP를 적용해보자

1. 스프링 AOP구현1

가장 단순한 방법으로 구현해보자

    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint)throws Throwable{
        log.info("[log] {}", joinPoint.getSignature()); // join point 시그니쳐
        return joinPoint.proceed();
    }
  • 조인포인트 시그니처를 로그로 남기는 로직이다.

  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.

  • "execution( hello.aop.order..(..))"
    포인트컷의 역할을 한다.
    hello.aop.order와 그 하위패키지를 지정한다.

  • @Around
    메서드 단위로 붙어 해당 메서드가 어드바이스가 된다.

  • @Aspect는 컴포넌트 스캔이 되지는 않으므로 따로 스프링빈으로 등록해줘야 한다.

Test 수정

  • @Aspect를 빈으로 등록해주자.
@Slf4j
@SpringBootTest
@Import(AspectV1.class)
public class AopTest {
  • 빈으로 등록하는 방법은
    @Component
    @Bean
    @Import() - 주로 Config파일을 추가할 때 사용
    등이 있는데 여기서는 간단하게 import를 이용해 빈으로 등록해주자.

이제 프록시 생성 여부를 확인해보면

프록시가 생성된 것을 확인할 수 있다.

2. AOP구현2 - 포인트컷 분리

@Slf4j
@Aspect
public class AspectV2 {

    //hello.aop.order. 패키지와 그 하위 패키지
    @Pointcut("execution(* hello.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();
    }
}
  • @Pointcut : 포인트컷 표현식을 사용한다.
    이 뒤에 오는 메서드를 포인트컷으로 사용하는데,
    반환타입은 void이어야 하고,
    메서드 내부는 비워둬야 함.
  • @Around("allOrder()")
    @Around 등의 어드바이스에서는 1번 예제처럼 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.
  • @Pointcut을 사용할 때 메서드의 접근 제어자public, private등의 영향을 받기 때문에 고려해서 사용해야 한다.

3. AOP구현3 - 어드바이스 추가

@Slf4j
@Aspect
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());
        }
    }
}
  • 2번 예제에서 포인트컷과 어드바이스 하나를 추가하였다.
  • doTransaction 어드바이스'@Around()'를 보면
    "allOrder() && allService()" 이 두가지의 포인트컷을 만족하는 대상에 어드바이스를 적용한다.
  • 반면 doLog 어드바이스는
    allOrder()를 만족하는 타겟에 어드바이스를 적용한다.

이 경우 로직의 흐름은 다음과 같다.

  • 그럼 어드바이스 적용 순서를 바꾸려면 어떻게 해야할까?
    5번 예제를 참고하자.

4. AOP구현4 - 포인트컷 참조

  • 별도의 클래스에 포인트컷을 모아서 관리하는 방법

포인트컷

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() {}
}

어드바이스

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

    // hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
    @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());
        }
    }
}
  • 외부의 포인트컷을 사용하기 위해서는 @Around(패키지명.포인트컷명)
    이런식으로 명시해주면 된다.

5. AOP구현5 - 어드바이스 적용 순서

어드바이스는 기본적으로 순서를 보장하지 않는다.
다만 @Apect단위로 어드바이스의 순서를 지정할 수 있다.
@Aspect는 클래스 단위로 붙기 때문에 같은 @Aspect에 있는 어드바이스끼리는 순서를 보장할 수 없고,
이럴 경우 별도의 @Order()를 이용해 별도의 클래스로 나눠서 관리해야한다.

Order() : 파라미터의 수가 작을수록 우선순위가 높다.

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

그럼 순서를 지정하기 전과 후의 결과를 확인해보자.

  • 어드바이스 순서 조정하기 전

    빨간색은 doLog(), 노란색은 doTransaction()이다.

  • 어드바이스 순서 조정 후

    @Order()로 정한 우선순위에 따라 어드바이스가 적용되는 것을 확인할 수 있다.

이것으로 여러가지 AOP 구현 방법에 대해서 알아보았다.
가장 기본적인 @Aspect 형태, 다수의 포인트컷, 포인트컷을 별도로 관리하고 적용하는 방법, @Order를 이용해 어드바이스 순서를 지정하는 방법 등에 대해서 알아보았다.
다음 포스팅에서는 @Around를 포함한 어드바이스의 종류에 대해서 알아보자.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글