AOP는 Aspect-Oriented Programming의 약자로 관점 지향 프로그래밍을 의미합니다. 객체 지향 프로그래밍(OOP)을 보완하는 개념으로, 애플리케이션의 핵심 기능(Core Concern)과 부가 기능(횡단 관심사)을 분리하여 모듈화하는 것이 목표입니다. 간단히 말해, 여러 클래스에 공통으로 적용되는 보조 기능(예: 로깅, 보안, 트랜잭션 등)을 별도 모듈로 만들고, 필요할 때 결합하는 방식입니다. OOP에서 클래스가 모듈화의 단위라면, AOP에서는 Aspect(관점)가 모듈화의 단위가 됩니다.
AOP에서는 각 관심사에 따라 코드를 모듈화합니다. 애플리케이션을 개발하다 보면 여러 부분에서 반복되는 코드를 발견할 수 있는데, 이러한 반복적이고 공통적인 기능을 횡단 관심사(Cross-cutting Concern)라고 합니다. AOP를 활용하면 이러한 횡단 관심사를 분리하여 관리하고, 핵심 비즈니스 로직(핵심 관심사)과 깨끗하게 분리할 수 있습니다.
@Aspect 애노테이션이 붙은 클래스로 구현합니다ProceedingJoinPoint.proceed()를 호출하여 원본 메서드를 실행할지 말지 결정할 수 있습니다.)execution(* com.example.service.UserService.getUser(..)) 같은 형식으로 메서드 패턴을 지정).IsModified라는 인터페이스를 구현하도록 도입하면, 해당 객체가 변경되었는지 여부를 추적하는 메서드를 추가할 수 있습니다. (Introduction은 AspectJ 용어로 인터타입 선언이라고도 합니다.)스프링 프레임워크의 핵심은 IoC 컨테이너를 통한 객체 관리이지만, AOP를 활용하면 이를 보완하여 애플리케이션의 중복 코드를 제거하고 구조를 개선할 수 있습니다. Spring에서 AOP는 선택 사항이며, IoC/DI를 사용하는 데 필수적이지는 않습니다. 그러나 AOP를 결합하면 스프링 기반 미들웨어 솔루션을 더욱 강력하고 깔끔하게 만들 수 있습니다. 예를 들어, 보일러플레이트(반복되는 상투적인 코드)를 Aspect로 분리함으로써 핵심 비즈니스 로직에만 집중할 수 있습니다.
Spring AOP에서는 개발자가 직접 커스텀 Aspect를 정의할 수 있도록 두 가지 방식을 지원합니다:
<aop:config> 등의 태그를 사용하여 Aspect와 어드바이스를 정의하는 방식입니다. (Spring 2.x 시대부터 제공되던 전통적인 방법)@Aspect 애노테이션을 붙이고, 어드바이스 메서드에 @Before, @AfterReturning 등의 애노테이션을 붙여서 Aspect를 정의하는 현대적인 방식입니다.@Transactional 애노테이션만 붙이면 메서드 앞뒤로 트랜잭션 시작과 종료를 자동 수행하거나, @Cacheable만 붙이면 캐싱 로직을 알아서 적용해주는 등, 부가 기능을 일일이 코드로 작성하지 않고 선언적으로 적용할 수 있습니다. 이러한 기능들은 대부분 Spring AOP를 기반으로 구현되어 있습니다.참고: 트랜잭션이나 보안 같은 기본 제공 기능만 사용한다면, Spring AOP를 직접 다룰 필요는 없습니다. Spring Boot에서 관련 starter를 추가하고 애노테이션을 선언하기만 해도 내부적으로 AOP 프록시가 설정되어 동작합니다. 하지만 AOP의 동작 방식이나 개념을 이해하면 이러한 기능들의 내부 동작을 파악하고 문제 발생 시 대응하는 데 도움이 됩니다.
앞서 언급한 AspectJ 포인트컷 표현식을 사용하면 메서드 패턴(execution), 타입 패턴(within), 객체 이름(bean), 어노테이션(@annotation) 등 다양한 기준으로 어드바이스 적용 대상을 정밀하게 지정할 수 있습니다. 일반적으로 가장 많이 쓰는 것은 execution() 지시자를 통한 메서드 명시이지만, 그 밖에도 유용한 지시자가 많습니다. 예를 들어:
bean(beanName): 특정 스프링 빈의 이름으로 대상 지정이 가능합니다. 예를 들어 @Before("bean(userService)")와 같이 하면, 스프링 컨테이너에 등록된 이름이 userService인 빈의 모든 메서드 실행 전에 어드바이스를 적용합니다.@annotation(AnnotationClass): 메서드에 특정 어노테이션이 붙어있는 경우에만 어드바이스를 적용할 수 있습니다. 예를 들어 @Before("@annotation(com.example.annotation.Logged)")와 같이 포인트컷을 정의하면, @Logged 애노테이션이 붙은 메서드가 실행될 때만 해당 어드바이스가 수행됩니다.이처럼 Spring AOP는 다양한 포인트컷 지시자를 통해 유연하게 대상을 선별할 수 있습니다. 포인트컷 표현식 문법은 AspectJ와 동일하므로, 필요에 따라 args(), this, target, within 등도 활용할 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.getUser(..))")
public void logBeforeUserGet() {
System.out.println("Getting user...");
}
}
@Aspect
@Component
public class TransactionAspect {
@AfterReturning("execution(* com.example.service.ProductService.*(..))")
public void commitTransaction() {
System.out.println("Committing transaction...");
}
@AfterThrowing("execution(* com.example.service.ProductService.*(..))")
public void rollbackTransaction() {
System.out.println("Rolling back transaction due to exception...");
}
}
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.controller.AdminController.*(..))")
public void checkAdminPermission() {
System.out.println("Checking admin permission...");
}
}
@Aspect
@Component
public class CachingAspect {
@AfterReturning(pointcut = "execution(* com.example.service.CacheService.*(..))", returning = "result")
public void cacheMethodResult(Object result) {
// Cache the result...
System.out.println("Caching method result...");
}
}
@Aspect
@Component
public class ExceptionLoggingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.PaymentService.*(..))", throwing = "exception")
public void logException(Exception exception) {
System.out.println("Exception caught: " + exception.getMessage());
}
}
@Aspect
@Component
public class PerformanceMonitoringAspect {
@Around("execution(* com.example.service.AnalyticsService.*(..))")
public Object measureExecutionTime(org.aspectj.lang.ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Method execution time: " + (endTime - startTime) + "ms");
return result;
}
}
@Transactional: 메서드에 이 애노테이션을 붙이면, 스프링이 해당 빈에 대해 프록시를 생성하여 트랜잭션 시작과 종료를 관리합니다. 프록시는 메서드 실행 전에 트랜잭션을 시작하고, 정상 종료하면 커밋(commit)하거나 예외 발생 시 롤백(rollback)합니다. 개발자는 단순히 애노테이션 선언만으로 트랜잭션 처리를 얻게 되는데, 이것이 가능한 이유가 바로 Spring AOP 프록시 덕분입니다.@Async: 이 애노테이션이 붙은 메서드는 별도의 스레드 풀에서 비동기로 실행됩니다. Spring은 프록시를 통해 해당 메서드 호출을 가로채고, 즉시 리턴시키면서 백그라운드 스레드에서 실제 메서드를 수행합니다. 이 역시 AOP 프록시가 없으면 구현하기 어려운 기능을 편리하게 제공하는 사례입니다.@Cacheable/@CacheEvict: 캐싱 관련 애노테이션들도 AOP로 동작합니다. @Cacheable의 경우 프록시가 대상 메서드를 호출하기 전에 캐시에 결과가 있는지 조회하고, 있으면 아예 대상 메서드를 실행하지 않고 캐시 값을 반환합니다. 없으면 메서드를 실행한 후 반환값을 캐시에 저장합니다. @CacheEvict는 메서드 실행 후에 캐시를 제거하는 로직을 프로키시가 수행합니다. 이러한 캐싱 로직은 모두 부가적인 관심사이며, AOP를 통해 핵심 로직과 분리되어 투명하게 처리됩니다.@PreAuthorize, @Secured): Spring Security에서는 메서드에 대한 사전 권한 체크도 AOP를 통해 구현됩니다. 해당 애노테이션이 붙은 메서드를 가진 빈은 프록시로 생성되며, 메서드가 호출될 때 프록시가 먼저 SecurityContext를 확인하여 올바른 권한이 있는지 검사합니다. 권한이 없으면 예외를 던지고, 권한이 있으면 실제 메서드를 호출합니다.Reference