개요
스프링 AOP (1) 편에서는 JDK 동적 프록시, CGLIB와 스프링 프록시, 빈 후기처리를 통해 프록시의 생성과정 및 작동 방법에 대해 배웠습니다. 이번에는 스프링에서 AOP의 개념 및 사용법에 대해서 알아보겠습니다.
AOP(Aspect Of Programming)이란?
AOP의 Aspect는 '관점'이라는 뜻으로 AOP는 이름 그대로 애플리케이션에서 기능 하나하나를 횡단 관심사의 관점으로 보는 것입니다. AOP는 OOP 지향 프로그래밍에서 횡단 관심사를 깔끔하게 처리하기 위해 등장했습니다.
핵심 기능과 분리하여 공통의 부가 기능들을 AOP에 정의합니다. 이를 Aspect라 하며 스프링에서 제공하는 advisor는 advice(부가 기능 정의)와 pointcut(어디에 적용 할 것인가)을 가지고 있어
pointcut 분리하기
@Pointcut은 반환타입이 void이며 내용은 비워두어야 합니다. @Around advice에 작성해도 되지만 좀 더 가독성을 높이기 위해서 별도의 pointcut으로 분리해서 사용하는 것이 편합니다.
아래 코드에서 @Pointcut을 allOrder() 라는 메서드와 allService() 라는 메서드에 정의합니다. 해당 정의는 @Around 바로 안에 넣을 수 있으나 코드가 길어지는 경우 가독성이 좋지 않으므로 따로 분리가 가능합니다.
//1. 구현부
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){}
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
//2. 사용부
@Around("allOrder()")
@Around("allOrder() && allService()")
아래 코드에서 2개의 pointcut을 합쳐서 새로운 pointcut을 만듭니다. 유연하게 활용할 수 있습니다.
//allOrder && allService를 합친 구현부
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
//사용부
@Around("orderAndService()")
pointcut 순서 정하기
pointcut은 순서를 보장하지 않습니다. 실행환경이나 JVM에 따라서 달라집니다. 순서를 정하기 위해서는 @Aspect를 사용해야 하며 클래스 단위로 순서를 정해주어야 합니다.
아래 코드에서 @Order(1)이 먼저 실행되고 @Order(2)가 실행됩니다. 클래스별로 분리해서 @Aspect를 사용합니다.
@Slf4j
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());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@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());
}
}
}
}
Advice 종류
Advice에는 총 5가지의 종류가 있습니다.
1.@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스
2.@Before : 조인 포인트 실행 이전에 실행
3.@AfterReturning : 조인 포인트가 정상 완료 후 실행
4.@AfterThrowing : 메서드가 예외를 던지는 경우 실행
5.@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
- @Around 외에 다른 어드바이스가 존재하는 이유는?
@Around 하나만 있어도 모든 기능을 수행 할 수 있지만, @Around는 항상 joinPoint.proceed()를 호출해야 합니다.
만약 아래 코드 1번처럼 joinPoint.proceed()를 호출하지 않는다면, 스프링 AOP가 동작하지 않고 target을 호출하지 않습니다. 하지만, @Before를 사용하면 실수할 걱정 없이 코드를 안전하게 짤 수 있습니다.
//1. Around 잘못된 예제
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
//joinPoint.proceed();
}
//2. Before 예제
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
또한 @Before를 보면 사용자 입장에서 "아 이 코드는 target 호출 전에 실행하는구나" 라고 알 수 있습니다. 모든 코드가 @Around로 되어있으면 구체적으로 코드를 확인해야 하지만, @Before, @After 등의 코드를 사용하면 어디에 사용되는지 구체적으로 알려주어서 가독성이 좋습니다. 항상 역할을 분명히 구분 지을 수 있도록 제약을 두는 코딩이 중요합니다.
로그 예제 코드로 실습 - AOP 구축하기
스프링 AOP를 사용해서 간단한 로그 예제를 만들어보겠습니다.
@Service
@RequiredArgsConsctructor
public class ExamService {
private final ExamRepository exampRepository;
@Trace
public void request(String itemId) {
examRepository.save(itemId);
}
}
@Repository
public class ExamRepository {
private static int seq = 0;
public String save(String itemId) {
seq++;
if(seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok"
}
}
Service, Repository를 만드는데, save() 메서드에서 seq가 5이면 예외를 발생하는 것이 포인트입니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}
@Aspect
public class TraceAspect {
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
}
@interface를 이용해 Trace를 어노테이션으로 만듭니다.
@Target에서 ElemenType.METHOD는 메서드에 적용한다는 의미입니다.
@Retention에서 RetentionPolicy.RUNTIME은 런타임에 적용한다는 의미입니다.
TraceAspect 클래스의 @Aspect를 추가해 동작을 정의합니다
@Before로 Trace 어노테이션이 붙은 메서드를 시작하기 전에 동작을 정의한다고 구체화합니다
joinPoint.getrSignature()와 joinPoint.getArgs()로 로그 기록을 남깁니다.
@Import(TraceAspect.class)
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
for(int i=0; i<5; i++) {
examService.request("data" + i);
}
}
}
junit을 이용해 테스트를 만들어서 총 5번의 request() 메서드를 실행합니다.
save() 메서드에서 예외가 발생하여 실패합니다.
로그 예제 코드로 실습 - 재시도 개선하기
위에서는 강제로 예외를 발생시켰는데 실무에서 예상치 못한 곳에서 예외가 발생할 수 있습니다. 따라서 예외가 발생할 때 재시도를 할 수 있도록 스프링 AOP를 이용해보겠습니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int value() default 3;
}
@Aspect
public class RetryAspect {
@Around("@annotation(retry)")
public void doTrace(ProceedingJoinPoint joinPoint, Retry retry) {
log.info("[retry] {} retry={}", joinPoint.getSignature(), retry);
int maxRetry = retry.value();
Exception exceptionHolder = null;
for(int retryCount=1; retryCount<=maxRetry; retryCount++) {
try {
log.info("[retry] try count={}/{}", retryCount, maxRetry);
return joinPoint.proceed();
} catch(Exception e){
exceptionHolder = e;
}
}
throw exceptionHolder;
}
}
Retry도 Trace와 마찬가지로 메서드에 사용 가능하며 런타임 때 사용하도록 만듭니다.
value()로 기본 값을 정의 할 수 있습니다.
@Aspect를 붙인 RetryAspect 클래스에서 동작을 정의합니다
@Around를 사용해 메서드 진입 시, 끝날 시 모두 확인하도록 합니다.
doTrace(...Retry retry) 메서드에 retry가 있기 때문에 @Around("@annotation(retry)")로 축약이 가능합니다
또한 retry.value() 메서드로 Retry 클래스에서 정의한 value를 사용할 수 있습니다.
해당 기본값을 이용해 for문에서 재시도를 합니다.
return joinPoint.proceed() 메서드를 for문에 넣기 때문에 정해진 횟수만큼 다시 실행할 수 있습니다.
@Service
@RequiredArgsConsctructor
public class ExamService {
private final ExamRepository exampRepository;
@Trace
@Retry
public void request(String itemId) {
examRepository.save(itemId);
}
}
@Trace에 @Retry를 추가합니다. 해당 메서드가 예외가 발생하면 총 3번까지 시도합니다.
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
for(int i=0; i<5; i++) {
examService.request("data" + i);
}
}
}
@Import({TraceAspect.clas, RetryAspect.class})로 AOP 동작을 정의했던 설정을 활성화합니다.
request() 메서드에서 i=4인 경우 예외가 발생하지만, 재시도를 통해서 해결을 합니다.
'Spring' 카테고리의 다른 글
스프링 AOP (3) - 실무 주의사항 (0) | 2023.02.02 |
---|---|
스프링 트랜잭션 이해 (0) | 2022.12.01 |
트랜잭션의 역사 (0) | 2022.09.04 |
@Controller @RestContrller (0) | 2022.08.25 |
@Controller, @Service, @Repository 차이 (0) | 2022.08.25 |