객체마다 보안, 로깅, 트랜잭션 등 특정한 기능을 구현한다면 너무나 많은 중복이 발생한다. 핵심 비즈니스 로직에 다양한 관점의 로직을 추가하게 되면 코드가 복잡해지고, 유지보수하기 어려워진다.
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
- 접근제어자, 반환타입, 메소드 선언 타입, 메소드 이름, 메소드 파라미터 타입, 메소드에서 발생시킬 수 있는 예외타입을 지정할 수 있다.
@Pointcut("@annotation(com.example.Loggable)")
@Service
@Slf4j
public class AddtionService {
private final AdditionStrategy additionStrategy;
public AddtionService(AdditionStrategy additionStrategy) {
this.additionStrategy = additionStrategy;
}
public int executeAddition(int start, int end) {
return additionStrategy.addNumbers(start, end);
}
}
public interface AdditionStrategy {
int addNumbers(int start, int end);
}
@Component
@Primary
public class LoopAdditionStrategy implements AdditionStrategy {
@Override
public int addNumbers(int start, int end) {
int ret = 0;
for (int i = start; i <= end; ++i) {
ret += i;
}
return ret;
}
}
@Component
public class RecursiveAddtionStrategy implements AdditionStrategy {
@Override
public int addNumbers(int start, int end) {
if (start > end) {
return 0;
}
return start + addNumbers(start + 1, end);
}
}
@Configuration
public class AppConfig {
@Bean
public AdditionService loopAdditionService(LoopAdditionStrategy loopAdditionStrategy) {
return new AdditionService(loopAdditionStrategy);
}
@Bean
public AdditionService recursiveAdditionService(RecursiveAddtionStrategy recursiveAddtionStrategy) {
return new AdditionService(recursiveAddtionStrategy);
}
}
@SpringBootTest
class AopTestApplicationTests {
@Autowired
private AdditionService loopAdditionService;
@Autowired
private AdditionService recursiveAdditionService;
@Test
void executeAdditionTest() {
int ret1 = loopAdditionService.executeAddition(0, 10);
int ret2 = recursiveAdditionService.executeAddition(0, 10);
Assertions.assertThat(ret1).isEqualTo(55);
Assertions.assertThat(ret2).isEqualTo(55);
}
}
생성자로 직접 주입하게 되면 Junit5가 @SpringBootTest 어노테이션으로 스프링에 등록된 빈을 가져오는 과정에서 어떤 빈을 가져올 지 몰라 ParameterResolutionException에러가 발생한다. @Autowired로 스프링에 직접 빈을 등록해 주었다.
테스트는 성공하지만, loopAdditionalService가 LoopAddtionalStrategy의 addNumbers를 호출했는지 미심쩍을 수 있다. 객체마다 로그를 찍어볼 수도 있겠지만, AOP를 적용하면 하나의 관점만 정의하면 된다. 로깅이라는 관점을 여러 객체에서 사용하면 되기 때문이다.
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Pointcut("execution(* com.core.*.addNumbers(*,*))")
private void targetMethod() {};
@Before("targetMethod()")
public void logBefore(JoinPoint joinPoint) {
log.info("[메서드 호출 전] 호출 클래스: " + joinPoint.getTarget().getClass().getSimpleName());
log.info("[메서드 호출 전] 호출 메서드: " + joinPoint.getSignature().getName());
}
@After("targetMethod()")
public void logAfter(JoinPoint joinPoint) {
log.info("[메서드 호출 후] 호출 메서드: " + joinPoint.getSignature().getName());
}
}
// 실행 결과
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 클래스: LoopAdditionStrategy
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 메서드: addNumbers
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 후] 호출 메서드: addNumbers
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 클래스: RecursiveAddtionStrategy
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 메서드: addNumbers
INFO 10661 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 후] 호출 메서드: addNumbers
@Aspect
@Component
@Slf4j
public class TimeAspect {
@Pointcut("execution(* com.core.*.addNumbers(*,*))")
private void targetMethod() {};
@Around("targetMethod()")
public Object calculateTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.nanoTime();
Object result = null;
try {
result = joinPoint.proceed(); // 메서드 실행 및 반환 값 캡쳐
} finally {
long endTime = System.nanoTime();
Signature signature = joinPoint.getSignature();
System.out.printf("호출 클래스: %s\n", joinPoint.getTarget().getClass().getSimpleName());
System.out.printf("실행 시간: %d ns\n", (endTime - startTime));
}
return result;
}
}
호출 클래스: LoopAdditionStrategy
실행 시간: 18208 ns
호출 클래스: RecursiveAddtionStrategy
실행 시간: 4875 ns
@Around("annotation(RollbackOnException)")
public Object rollbackOnException(ProceedingJoinPoint joinPoint) throws Throwable {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
Object result = joinPoint.proceed();
transactionManager.commit(status);
return result;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
@Around("@annotation(RetryableOperation)")
public Object retryOperation(ProceddingJoinPoint joinPoint) throws Throwable {
int retryCount = 3;
while (retryCount > 0) {
try {
return joinPoint.proceed();
} catch (Exception e) {
log.warn("Operation failed, retrying...");
retryCount--;
}
}
throw new RuntimeException("Failed after multiple attempts");
}
@Around("execution(* com.example.external.ExternalService.*(..))")
public Object translateException(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (ExternalDependencyException e) {
throw new ApplicationSpecificException("An error occured while using external service");
}
}
위의 로깅 AOP를 적용하는 코드에서 Pointcut을 다음과 같이 수정하여 @Pointcut("execution( com.core.AppConfig.(..))") AppConfig 빈이 설정되는 과정을 볼 수 있다.
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 클래스: AppConfig$$SpringCGLIB$$0
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 메서드: loopAdditionService
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 후] 호출 메서드: loopAdditionService
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 클래스: AppConfig$$SpringCGLIB$$0
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 전] 호출 메서드: recursiveAdditionService
INFO 11836 --- [ Test worker] com.core.LoggingAspect : [메서드 호출 후] 호출 메서드: recursiveAdditionService