[스프링 AOP] 실습
스프링 핵심 원리 - 고급편 - 인프런 | 강의
이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다.
2023.01.30 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 개념
앞에서 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());
// 조인포인트는 자동으로 실행
}
메소드 종료시 자동으로 다음 타겟이 호출 됩니다.@Around
는 ProceedingJoinPoint.proceed()
를 호출해야 다음 대상이 호출되지만, @Before
는 ProceedingJoinPoint.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
는 넓은 기능을 제공하지만 개발자의 실수를 막을 수는 없다.
그래서 기능에 제약이 있는 다른 어드바이스를 제공합니다.
기능에 제약이 있기 때문에 역할이 분명해집니다.
이러한 제약으로 인해 개발자의 실수를 방지 할 수 있습니다.
'0+ 스프링 > 0+ 스프링 AOP' 카테고리의 다른 글
[스프링 AOP] 프록시 타입 캐스팅 한계 (0) | 2023.02.07 |
---|---|
[스프링 AOP] 내부 호출 문제 해결 (0) | 2023.02.07 |
[스프링 AOP] 포인트컷 지시자 (0) | 2023.02.03 |
[스프링 AOP] 개념 (0) | 2023.01.30 |