0+ 스프링/0+ 스프링 AOP

[스프링 AOP] 실습

힘들면힘을내는쿼카 2023. 1. 31. 12:10
728x90
반응형

[스프링 AOP] 실습

스프링 핵심 원리 - 고급편 - 인프런 | 강의
이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다.

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

2023.01.30 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 개념

 

[스프링] AOP 개념

[스프링] AOP 개념 스프링 핵심 원리 - 고급편 - 인프런 | 강의 이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다. 스프링 핵심 원리 - 고급편 - 인프런 | 강의 스프링의

howisitgo1ng.tistory.com

 

앞에서 AOP의 개념에 대해서 배웠습니다.
이번에는 스프링 AOP를 적용한 예제 코드를 작성해보겠습니다.

 

애플리케이션 로직은 크게 핵심 기능부가 기능으로 나눌 수 있다고 AOP 개념에서 말씀드렸습니다.

먼저 간단하게 핵심 기능에 해당하는 결제서비스를 만들어보겠습니다.

 

핵심 기능

결제서비스는 PaymentRepository, PaymentService로 구성되어 있습니다.

PaymentRepository

@Slf4j
@Repository
public class PaymentRepository {

    public void save(int price) {
        log.info("[PaymentRepository 실행]={}", price);
        if(price < 0) {
            throw new IllegalStateException("[PaymentRepository 예외발생]");
        }
    }
}

PaymentService

@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;

    public void payment(int price) {
        log.info("[PaymentService 실행]");
        paymentRepository.save(price);
    }
}

스프링 AOP 구현

자, 결제시스템에 로그와 실행시간을 추적할수 있는 로직을 추가해 달라고 요청이 왔습니다.!
이때 핵심기능 실행시간 추적 이후에 로그를 찍어달라고 했습니다.

정리해보면 다음과 같습니다.(괄호 뒤 숫자는 순서 입니다.)

  • 로직 실행시간 측정(1)
  • 로그 추적(2)

스프링 AOP를 사용하여 개발해 봅시다.🧑‍💻

패키지 구조

PointCutsPayment

public class PointCutsPayment {

    // spring.advanced.advancedspring.payment 하위 패키지
    @Pointcut("execution(* spring.advanced.advancedspring.payment..*(..)) " +
            "&& !target(spring.advanced.advancedspring.payment.aop.AopConfig)")
    public void allPayment(){}

    // 클래스 이름 패턴이 *Repository
    @Pointcut("execution(* *..*Repository.*(..)) ")
    public void allRepository(){}

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

    // spring.advanced.advancedspring.payment 하위 패키지이고, 클래스 이름 패턴이 *Repository
    @Pointcut("allPayment() && allRepository()")
    public void paymentAndRepository(){}

    // spring.advanced.advancedspring.payment 하위 패키지이고, 클래스 이름 패턴이 *Service
    @Pointcut("allPayment() && allService()")
    public void paymentAndService(){}
}

스프링 AOP를 구현하는 일반적인 방법은 @Aspect를 사용하는 방법 입니다.
@Apect를 사용하여 하나의 클래스에 포인트컷과 어드바이스를 구현 할 수 있지만, 여기서는 포인트컷을 따로 분리했습니다.

현재 패키지 구조가 payment 하위에 aop.AopConfig가 존재합니다.
순환참조 문제를 막기 위해서 !target(spring.advanced.advancedspring.payment.aop.AopConfig 같이 설정합니다.

AspectPayment

@Slf4j
public class AspectPayment {

    @Aspect
    @Order(1) // 순서
    public static class TimeAspect {
        @Around("spring.advanced.advancedspring.payment.aop.PointCutsPayment.paymentAndService()")
        public Object doTime(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                long startTime = System.currentTimeMillis();
                log.info("{} [시간측정 시작]", joinPoint.getSignature());

                Object result = joinPoint.proceed(); // 조인 포인트

                long endTime = System.currentTimeMillis();
                log.info("{} [걸린 시간] {}ms", joinPoint.getSignature(), endTime - startTime);

                return result;
            } catch (Exception e) {
                log.info("{} [시간측정 불가] 예외 발생 {}", joinPoint.getSignature(), e.getMessage());
                throw e;
            } finally {
                log.info("{} [시간측정 종료]", joinPoint.getSignature());
            }
        }
    }

    @Aspect
    @Order(2) // 순서
    public static class LogAspect {
        @Around("spring.advanced.advancedspring.payment.aop.PointCutsPayment.allPayment()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[LOG] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

기본적으로 어드바이스는 순서를 보장하지 않습니다.
순서를 지정하고 싶으면 @Aspect 적용 단위로 @Order를 적용해야 합니다.
그런데…. 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점 입니다..!
그래서 @Aspect별도의 클래스로 분리 했습니다. 그리고 @Order를 통해 실행 순서를 적용했습니다.

AopConfig

@Configuration
public class AopConfig {

    @Bean
    public AspectPayment.LogAspect logAspect() {
        return new AspectPayment.LogAspect();
    }

    @Bean
    public AspectPayment.TimeAspect timeAspect() {
        return new AspectPayment.TimeAspect();
    }
}

어드바이스를 빈으로 등록 합니다.

AopPaymentTest

@Slf4j
@SpringBootTest
public class AopPaymentTest {

    @Autowired
    PaymentRepository paymentRepository;
    @Autowired
    PaymentService paymentService;

    @Test
    void appInfo() {
        log.info("isAopProxy, paymentService={}", AopUtils.isAopProxy(paymentService));
        log.info("isAopProxy, paymentRepository={}", AopUtils.isAopProxy(paymentRepository));
    }

    @Test
    void exception() {
        Assertions.assertThatThrownBy(() -> paymentService.payment(-100))
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    void success() {
        paymentService.payment(100);
    }
}

테스트 코드를 작성했습니다.

결과

appInfo

isAopProxy, paymentService=true
isAopProxy, paymentRepository=true

exception

void spring.advanced.advancedspring.payment.PaymentService.payment(int) [시간측정 시작]
[LOG] void spring.advanced.advancedspring.payment.PaymentService.payment(int)
[PaymentService 실행]
[LOG] void spring.advanced.advancedspring.payment.PaymentRepository.save(int)
[PaymentRepository 실행]=-100
void spring.advanced.advancedspring.payment.PaymentService.payment(int) [시간측정 불가] 예외 발생 [PaymentRepository 예외발생]
void spring.advanced.advancedspring.payment.PaymentService.payment(int) [시간측정 종료]

success

void spring.advanced.advancedspring.payment.PaymentService.payment(int) [시간측정 시작]
[LOG] void spring.advanced.advancedspring.payment.PaymentService.payment(int)
[PaymentService 실행]
[LOG] void spring.advanced.advancedspring.payment.PaymentRepository.save(int)
[PaymentRepository 실행]=100
void spring.advanced.advancedspring.payment.PaymentService.payment(int) [걸린 시간] 80ms
void spring.advanced.advancedspring.payment.PaymentService.payment(int) [시간측정 종료]

어드바이스 종류

어드바이스에는 여러가지 종류가 있습니다.
앞 예제에서 사용한 @Around외에도 여러가지 종류가 있습니다.

@Around

  • 메소드 호출 전후에 실행
  • 가장 강력한 어드바이스
    • 조인 포인트 실행 여부 선택 (joinPoint.proceed() // 호출 여부 선택)
    • 전달 값 반환 (joinPoint.proceed(arg[]))
    • 반환 값 변환
    • 예외 변환
    • 트랜잭션 처럼 try ~ catch ~ finally 모두 들어가는 구문 처리 가능
  • 어드바이스의 첫번째 파라미터는 ProceedingJoinPoint 사용
  • proceed()를 통해 대상 실행
  • proceed() 여러번 실행 가능
/**
 * 메소드 호출 전후에 수행
 * 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등 가능
 */
@Around("생략")
public Object doTime(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        // @Before
        long startTime = System.currentTimeMillis();
        log.info("{} [시간측정 시작]", joinPoint.getSignature());

        Object result = joinPoint.proceed(); // 조인 포인트

        // @AfterReturning
        long endTime = System.currentTimeMillis();
        log.info("{} [걸린 시간] {}ms", joinPoint.getSignature(), endTime - startTime);

        return result;
    } catch (Exception e) {
        // @AfterThrowing
        log.info("{} [시간측정 불가] 예외 발생 {}", joinPoint.getSignature(), e.getMessage());
        throw e;
    } finally {
        // @After
        log.info("{} [시간측정 종료]", joinPoint.getSignature());
    }
}

@Before

조인 포인트 실행 전

/**
 * 조인 포인트 실행 이전에 실행
 */
@Before("생략")
public void doBefore(JoinPoint joinPoint) {
    log.info("[before] {}", joinPoint.getSignature());
    // 조인포인트는 자동으로 실행
}

메소드 종료시 자동으로 다음 타겟이 호출 됩니다.
@AroundProceedingJoinPoint.proceed()를 호출해야 다음 대상이 호출되지만, @BeforeProceedingJoinPoint.proceed()를 사용하지 않는다.

@AfterReturning

메소드 실행이 정상적으로 반환될 때 실행

/**
 * 조인 포인트가 정상 완료후 실행
 */
@AfterReturning(value = "생략", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
    log.info("[return] {} return={}", joinPoint.getSignature(), result);
}

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

@AfterThrowing

메소드 실행이 예외를 던져서 종료될 때 실행

/**
 * 메소드가 예외를 던지는 경우 실행
 */
@AfterThrowing(value = "생략", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
    log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
    // throw e 자동으로 실행
}

@After

메소드 실행이 종료되면 실행

/**
 * 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
 */
@After("생략")
public void doAfter(JoinPoint joinPoint) {
    log.info("[after] {}", joinPoint.getSignature());
}

finally를 생각하면 쉽습니다.
일반적으로 리소스를 해제하는데 사용합니다.

어드바이스 우선 순위

스프링 버전 5.2.7부터 @Aspect안에서 동일한 조인 포인트의 우선순위를 정했습니다.
실행 순서:@Around, @Before,@After, @AfterReturning, @AfterThrowing

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

어드바이스를 보면 @Around가 모든 기능을 포함하고 있습니다.
그런데 다른 어드바이스들이 존재하는 이유는 무엇일까요? 🤔

만약 새로 오신분이 타겟 실행 전에 로그를 출력하기 위해 다음과 같이 코드를 작성했다고 합시다.

@Around("생략")
public void doBefore(ProceedingJoinPoint joinPoint) {
      log.info("[before] {}", joinPoint.getSignature());
}

무엇이 문제인지 보이시나요?
위 코드는 타겟을 호출하지 않았습니다..!
@Around는 항상 ProceedingJoinPoint.proceed()를 호출해야 합니다.
호출하지 않을 경우 🐞버그가 발생하겠죠?

이러한 개발자의 실수를 예방하기 위해 @Around외에 어드바이스를 제공합니다.

@Before("생략")
public void doBefore(JoinPoint joinPoint) {
      log.info("[before] {}", joinPoint.getSignature());
}

@Around는 넓은 기능을 제공하지만 개발자의 실수를 막을 수는 없다.
그래서 기능에 제약이 있는 다른 어드바이스를 제공합니다.
기능에 제약이 있기 때문에 역할이 분명해집니다.
이러한 제약으로 인해 개발자의 실수를 방지 할 수 있습니다.

 

 

 

728x90
반응형