웹 애플리케이션을 개발하다 보면 로깅, 보안, 트랜잭션 관리 같은 부가 기능들이 비즈니스 로직 곳곳에 반복적으로 나타나는 것을 볼 수 있습니다. 이러한 횡단 관심사(Cross-cutting Concerns)는 코드의 가독성을 떨어뜨리고 유지보수를 어렵게 만듭니다.
AOP(Aspect Oriented Programming)는 이런 문제를 해결하기 위해 등장한 프로그래밍 패러다임입니다. 핵심 비즈니스 로직과 부가 기능을 분리하여, 부가 기능을 모듈화하고 필요한 곳에 선택적으로 적용할 수 있게 해줍니다.
'Aspect'라는 단어가 '관점'을 의미하듯이, AOP는 애플리케이션을 여러 관점에서 바라보는 프로그래밍 방식입니다.
부가 기능이 삽입될 수 있는 실행 지점을 의미합니다. 스프링 AOP에서는 메서드 실행 시점만을 조인 포인트로 지원합니다.
조인 포인트 중에서 실제로 어드바이스를 적용할 위치를 선별하는 필터 역할을 합니다. 주로 AspectJ 표현식을 사용하여 정의합니다.
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceLayer() {}
실제로 실행될 부가 기능 코드입니다. 실행 시점에 따라 여러 종류가 있습니다:
포인트컷과 어드바이스를 하나로 묶은 모듈입니다. @Aspect 어노테이션으로 정의합니다.
부가 기능이 적용될 실제 객체입니다.
AOP 기능 구현을 위해 생성되는 프록시 객체입니다. 스프링에서는 JDK 동적 프록시 또는 CGLIB 프록시를 사용합니다.
AOP를 적용하는 방식은 세 가지가 있습니다:
스프링 AOP는 런타임 시점의 프록시 방식을 채택합니다. 복잡한 설정 없이도 스프링이 자동으로 처리해주므로 가장 편리하게 사용할 수 있습니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // 실제 메서드 실행
return result;
} finally {
long endTime = System.currentTimeMillis();
System.out.println("메서드 실행시간: " + (endTime - startTime) + "ms");
}
}
}
@Around 어드바이스에서 사용되는 ProceedingJoinPoint는 실행 중인 메서드에 대한 다양한 정보를 제공합니다:
@Around("execution(* com.example.*.*(..))")
public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 정보 얻기
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
System.out.println("실행 메서드: " + className + "." + methodName);
System.out.println("전달 파라미터: " + Arrays.toString(args));
Object result = joinPoint.proceed();
return result;
}
| 메서드 | 설명 |
|---|---|
getSignature() | 호출되는 메서드의 시그니처 정보 |
getTarget() | 대상 객체 반환 |
getArgs() | 메서드 파라미터 배열 |
proceed() | 다음 어드바이스나 타겟 메서드 실행 |
스프링 AOP는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 통해 동작합니다:
@Aspect가 붙은 클래스를 어드바이저로 변환프록시가 생성되면서 원본 객체를 감싸고, 메서드 호출을 가로채어 부가 기능을 수행한 후 실제 메서드를 호출합니다.
private 메서드: AOP 적용 불가final 메서드: AOP 적용 불가static 메서드: AOP 적용 불가스프링 AOP는 프록시 기반이므로 외부에서 호출되는 메서드에만 적용됩니다. 같은 클래스 내부에서의 메서드 호출에는 AOP가 동작하지 않습니다.
@Service
public class UserService {
@Transactional // AOP 적용됨
public void saveUser(User user) {
validateUser(user); // 내부 호출 - AOP 적용 안됨
// ...
}
@Transactional
private void validateUser(User user) {
// 이 메서드의 @Transactional은 동작하지 않음
}
}
@Aspect
@Component
public class PerformanceAspect {
@Around("@annotation(Monitored)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
try {
return joinPoint.proceed();
} finally {
stopWatch.stop();
System.out.println(joinPoint.getSignature().getName() +
" 실행시간: " + stopWatch.getTotalTimeMillis() + "ms");
}
}
}
// 사용법
@Monitored
public class UserService {
public void processUser() {
// 비즈니스 로직
}
}
@Aspect
@Component
public class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void handleException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.err.println("메서드 " + methodName + "에서 예외 발생: " + ex.getMessage());
// 로깅, 알림 등의 추가 처리
}
}
AOP는 관심사의 분리를 통해 코드의 모듈성을 높이고 유지보수성을 개선하는 강력한 도구입니다. 스프링 AOP를 활용하면 비즈니스 로직에 집중하면서도 필요한 부가 기능들을 깔끔하게 적용할 수 있습니다.
다만 프록시 기반의 한계와 제약사항을 이해하고 적절한 상황에서 사용하는 것이 중요합니다. 과도한 AOP 사용은 오히려 코드의 복잡성을 증가시킬 수 있으니 균형있는 접근이 필요합니다.