Spring AOP로 로깅과 트랜잭션 처리


AOP(Aspect-Oriented Programming)는 횡단 관심사(로깅, 트랜잭션, 보안 등)를 비즈니스 로직과 분리하는 프로그래밍 패러다임입니다. Spring AOP로 코드 중복 없이 공통 기능을 적용할 수 있습니다.



언제 사용하나요?



  • 메서드 실행 시간 측정

  • 입출력 파라미터 로깅

  • 트랜잭션 관리

  • 권한 검사

  • 예외 처리 통합



AOP 용어


Aspect: 횡단 관심사 모듈 (로깅, 트랜잭션 등)
Join Point: Aspect 적용 가능 지점 (메서드 실행 등)
Pointcut: Join Point 선택 표현식
Advice: 실제 실행되는 코드 (Before, After, Around 등)
Target: Aspect가 적용되는 대상 객체


의존성 추가


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>


로깅 Aspect 예시


@Aspect
@Component
@Slf4j
public class LoggingAspect {

// 모든 Service 클래스의 모든 메서드
@Pointcut("execution(* com.example..service.*.*(..))")
public void serviceLayer() {}

// 메서드 실행 전
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
log.info("메서드 시작: {}.{}",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName());
log.info("파라미터: {}", Arrays.toString(joinPoint.getArgs()));
}

// 메서드 정상 종료 후
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("메서드 완료: {}, 반환값: {}",
joinPoint.getSignature().getName(), result);
}

// 예외 발생 시
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
log.error("메서드 예외: {}, 예외: {}",
joinPoint.getSignature().getName(), ex.getMessage());
}
}


실행 시간 측정


@Aspect
@Component
@Slf4j
public class PerformanceAspect {

@Around("execution(* com.example..controller.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint)
throws Throwable {

long startTime = System.currentTimeMillis();

try {
Object result = joinPoint.proceed();
return result;
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;

log.info("{}.{} 실행시간: {}ms",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
executionTime);

if (executionTime > 1000) {
log.warn("느린 메서드 감지: {}ms", executionTime);
}
}
}
}


커스텀 어노테이션 사용


// 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}

// Aspect에서 어노테이션 감지
@Aspect
@Component
public class CustomAnnotationAspect {

@Around("@annotation(loggable)")
public Object logAnnotatedMethod(ProceedingJoinPoint joinPoint,
Loggable loggable) throws Throwable {
log.info("@Loggable 메서드 시작: {} - {}",
joinPoint.getSignature().getName(),
loggable.value());
return joinPoint.proceed();
}
}

// 사용
@Service
public class UserService {

@Loggable("사용자 조회")
public User findById(Long id) {
return userRepository.findById(id);
}
}


트랜잭션 AOP


@Aspect
@Component
public class TransactionAspect {

@Autowired
private PlatformTransactionManager transactionManager;

@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint)
throws Throwable {

TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition());

try {
Object result = joinPoint.proceed();
transactionManager.commit(status);
return result;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}

// 실제로는 @Transactional 사용
@Service
public class OrderService {

@Transactional
public void createOrder(OrderDto dto) {
orderRepository.save(dto.toEntity());
inventoryService.decreaseStock(dto.getProductId());
paymentService.processPayment(dto.getPaymentInfo());
}
}


Pointcut 표현식


// 패키지 내 모든 메서드
execution(* com.example.service.*.*(..))

// 특정 클래스 모든 메서드
execution(* com.example.service.UserService.*(..))

// 특정 리턴 타입
execution(String com.example..*.*(..))

// 특정 파라미터
execution(* com.example..*.*(Long, String))

// 어노테이션
@annotation(org.springframework.transaction.annotation.Transactional)
@within(org.springframework.stereotype.Service)

// 조합
@Pointcut("execution(* com.example..*.*(..)) && !execution(* com.example..*.get*(..))")


주의사항



  • 같은 클래스 내부 호출은 AOP 적용 안됨 (프록시 한계)

  • private 메서드는 AOP 적용 불가

  • @Async와 함께 사용 시 순서 주의

  • AOP 남용 시 디버깅 어려움