Spring Boot

스프링 AOP 구현하기 - 1 (@Aspect, @Around, @Pointcut)

작은별._. 2024. 9. 30. 09:00
728x90

스프링 AOP를 구현하는 기본 코드입니다. 총 2개 포스팅으로 작성하였습니다.


build.gradle

스프링 AOP 기능을 구현하기 위해서 아래 dependency 들로 구성하였습니다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
    // Spring aop 사용을 위한 dependency
	implementation 'org.springframework.boot:spring-boot-starter-aop'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//테스트에서 lombok 사용
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

 


예제 프로젝트 만들기

전체 package 구조는 아래와 같습니다.



OrderService와 OrderRepository 클래스


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";
    }

}

테스트 코드

OrderService와 OrderRepository 클래스의 동작 방식을 테스트하는 간단한 테스트 코드를 작성하였습니다.


@Slf4j
@SpringBootTest
public class AopTest {
    @Autowired
    OrderService orderService;
    @Autowired
    OrderRepository orderRepository;

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

 

 

위와 같이 OrderService, OrderRepository 클래스를 구성 한 후에 스프링 AOP를 적용해 보겠습니다. 


스프링 AOP 구현 - 1 (@Around)

스프링 AOP를 구현하는 일반적인 방법은 @Aspect를 사용하는 방법입니다. 먼저 가장 단순한 AOP를 구현해 보도록 하겠습니다.


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 {

    //hello.aop.order 패키지와 하위 패키지
    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }
}

 

  • @Around 애너테이션 값인 인 execution(* hello.aop.order..*(..)) 는 포인트컷이 됩니다. ( execution(* hello.aop.order..*(..)) 는 AspectJ 포인트컷 표현식)
    • hello.aop.order 패키지 및 하위 패키지 내부의 빈의 모든 메서드가 AOP 적용의 대상이 됩니다.
  • @Around 애노테이션의 메서드인 doLog 는 어드바이스(Advice)가 됩니다.

 

간단한 테스트 코드 작성으로 hello.aop.order 패키지 내부의 OrderService와 OrderRepository의 메서드들이 aop의 적용 대상인지 여부를 확인해 보겠습니다.

 


AopTest 코드

@Slf4j
@Import(AspectV1.class) // 빈 등록!!
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        // AopUtils 로 AopProxy 적용 여부 확인
        log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
    }
    @Test
    void success() {
        orderService.orderItem("itemA");
    }
    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

 

여기서 중요한 점은 !! @Aspect는 그저 Aspect 라는 표현을 나타내는 애너테이션으로 component 가 아닙니다. 그래서 componentScan의 대상이 되지 않습니다. 따라서, AspectV1을 AOP로 사용하려면 스프링 빈으로 등록해야 합니다.

 

스프링 빈으로 등록하는 방법은 크게 3가지가 있습니다.

  • @Bean과 @Configuration 을 사용해서 직접 등록
  • @Component과 @ComponentScan을 사용해서 자동 등록
  • @Import : 주로 설정 파일(@Configuration 클래스)을 추가할 때 사용

@Import는 주로 설정 파일을 추가할 때 사용하지만,간단하게 bean을 생성하기 위해서 @Import를 사용하였습니다.

 

위 테스트 코드를 실행하면 아래와 같이 출력된 로그로 Aop 적용이 잘 되었음을 확인할 수 있습니다.

 

aopInfo() 메소드 실행
success() 메소드 실행


스프링 AOP 구현 - 2 (@Pointcut)

첫 번째 방법처럼 @Around에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있습니다.


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;

@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에 포인트컷 표현식을 사용합니다.
  • @Pointcut이 적용된 메서드의 반환 타입은 void여야 하고, 메서드 내용은 비워둡니다.
  • @Around 어드바이스에 포인트컷 시그니처를 사용하여 포인트컷을 지정합니다.
  • private , public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 Aspect에서 참고하려면 public을 사용해야 합니다.

결과적으로 AspectV1 과 같은 기능을 수행합니다. (AopTest 클래스 코드의 @Import 부분만 AspectV2로 변경한 후 수행해 보세요.)

이렇게 분리하면 하나의 포인트컷 표현식을 내부의 여러 어드바이스뿐만 아니라 외부 어드바이스에서도 함께 사용할 수 있습니다.

 

 

위와 같이 포인트 컷을 분리하여 조금 더 복잡한 AOP를 구현해 보겠습니다. 아래 코드에는 트랜잭션 동작을 로깅하는 기능을 추가하였습니다. 

  • allOrder() 포인트컷은 hello.aop.order 패키지와 하위 패키지를 대상으로 합니다. 따라서, OrderService와 OrderRepository 클래스 메서드에 모두 적용됩니다.
  • allService() 포인트컷은 클래스 및 인터페이스의 이름 패턴이 Service로 끝나는 것을 대상으로 합니다.

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;

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

}

 

위 코드에서 @Around("allOrder() && allService()") 와 같이 사용하고 있습니다. 포인트컷은 이렇게 조합도 가능합니다. 

(&&(AND), ||(OR),!(NOT) 조합 가능)

결국, 이 조합은 hello.aop.order 패키지와 하위 패키지이면서, 클래스나 인터페이스 이름 패턴이 Service로 끝나는 것을 대상으로 하라라는 의미이기 때문에, OrderService 클래스에만 적용됩니다.

  • orderService : doLog() , doTransaction() 어드바이스 적용
  • orderRepository : doLog() 어드바이스 적용


AopTest 코드를 아래처럼 수정한 후 실행한 후 결과입니다.

@Import(AspectV3.class)
@SpringBootTest
public class AopTest {
}

 

success() 메소드 실행 (커밋)
exception() 메소드 실행 (롤백)

 

다음 포스팅에는 AOP의 Advice가 적용되는 순서를 변경하고 싶을 때 어떻게 해야 하는지, 그리고 @Around 이외에 @Before, @After 등의 다양한 Advice의 종류에 대해서 작성해 보겠습니다~

 

전체 소스코드

https://github.com/eunhwa99/SpringAOP/tree/main/advanced/src/main/java/hello/aop

 

SpringAOP/advanced/src/main/java/hello/aop at main · eunhwa99/SpringAOP

SpringAOP 구현 코드. Contribute to eunhwa99/SpringAOP development by creating an account on GitHub.

github.com

 


[참고자료]

김영한, "스프링 핵심 원리 - 고급편 ", 인프런

 

728x90
반응형