Spring AOP(Aspect-Oriented Programming)란 무엇이며 주요 개념을 설명해주세요.
힌트
횡단 관심사, Aspect, Advice, Pointcut, JoinPoint를 생각해보세요.
정답 및 해설
Spring AOP(Aspect-Oriented Programming)란 무엇이며 주요 개념을 설명해주세요.
AOP(관점 지향 프로그래밍)는 핵심 비즈니스 로직과 횡단 관심사(Cross-Cutting Concerns)—로깅, 트랜잭션, 보안, 캐싱 등—를 분리하는 프로그래밍 패러다임입니다. 여러 클래스에 걸쳐 반복되는 부가 기능을 한 곳에 모아 관리함으로써 코드 중복을 제거하고 유지보수성을 높입니다.
AOP가 필요한 이유
로깅을 예로 들면, AOP 없이는 모든 서비스 메서드에 아래와 같은 코드를 반복 작성해야 합니다.
public void createOrder(OrderRequest request) {
log.info("createOrder 시작: {}", request); // 반복되는 로깅
// 비즈니스 로직
log.info("createOrder 종료"); // 반복되는 로깅
}
AOP를 사용하면 이 로깅 코드를 Aspect 하나로 분리하여 적용 대상 메서드에 자동으로 주입할 수 있습니다.
주요 개념
Aspect
횡단 관심사를 모듈화한 단위입니다. @Aspect 어노테이션으로 선언하며, Advice와 Pointcut의 조합으로 구성됩니다.
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
// Pointcut + Advice 조합
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("메서드 시작: {}", joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
log.info("메서드 종료: {}", joinPoint.getSignature().getName());
return result;
}
}
Advice
실제 부가 기능 코드가 담긴 메서드입니다. 실행 시점에 따라 5가지로 구분됩니다.
| 어노테이션 | 실행 시점 | 주요 사용 사례 |
|---|---|---|
@Before | 메서드 실행 전 | 파라미터 검증, 로깅 |
@After | 메서드 실행 후 (성공/실패 무관) | 리소스 정리 |
@AfterReturning | 메서드 정상 반환 후 | 반환값 로깅, 캐시 저장 |
@AfterThrowing | 예외 발생 후 | 에러 로깅, 알림 발송 |
@Around | 메서드 실행 전후 모두 | 트랜잭션, 성능 측정 |
@Aspect
@Component
public class ExampleAspect {
// 메서드 실행 전
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
log.info("[Before] 메서드: {}", joinPoint.getSignature().getName());
}
// 정상 반환 후 — 반환값 접근 가능
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
log.info("[AfterReturning] 반환값: {}", result);
}
// 예외 발생 후 — 예외 객체 접근 가능
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
log.error("[AfterThrowing] 예외: {}", ex.getMessage());
}
// 실행 전후 — proceed()로 원본 메서드 호출
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - start;
log.info("[Around] {}ms 소요", elapsed);
return result;
}
}
Pointcut
Advice가 적용될 위치(JoinPoint)를 정의하는 표현식입니다. @Pointcut으로 재사용 가능한 표현식을 선언할 수 있습니다.
@Aspect
@Component
public class PointcutExamples {
// 재사용 가능한 Pointcut 정의
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.annotation.Loggable)")
public void loggableMethod() {}
// 조합 사용
@Before("serviceLayer() || loggableMethod()")
public void combinedAdvice(JoinPoint joinPoint) {
log.info("결합된 Pointcut 적용: {}", joinPoint.getSignature());
}
}
주요 Pointcut 표현식:
| 표현식 | 설명 | 예시 |
|---|---|---|
execution | 메서드 실행 패턴 | execution(* com.example.service.*.*(..)) |
@annotation | 특정 어노테이션이 붙은 메서드 | @annotation(Transactional) |
within | 특정 타입 내 메서드 | within(com.example.service.*) |
@within | 특정 어노테이션이 붙은 타입 | @within(Service) |
args | 특정 파라미터 타입 | args(String, ..) |
bean | 특정 빈 이름 | bean(orderService) |
JoinPoint
Advice가 적용되는 실제 실행 지점입니다. Spring AOP에서는 메서드 실행만 JoinPoint로 지원합니다.
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
// 메서드 시그니처
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getName();
// 파라미터
Object[] args = joinPoint.getArgs();
// 대상 객체
Object target = joinPoint.getTarget();
log.info("클래스: {}, 메서드: {}, 파라미터: {}",
target.getClass().getSimpleName(), methodName, Arrays.toString(args));
}
Spring AOP의 동작 원리: 프록시 기반
Spring AOP는 실제 객체를 감싸는 프록시 객체를 생성해 Advice를 적용합니다.
클라이언트 → 프록시(Advice 실행) → 실제 Bean(핵심 로직)
프록시 생성 방식은 두 가지입니다.
| 방식 | 적용 조건 | 특징 |
|---|---|---|
| JDK Dynamic Proxy | 인터페이스가 있는 경우 | java.lang.reflect.Proxy 사용 |
| CGLIB | 인터페이스가 없거나 proxyTargetClass=true | 바이트코드 조작으로 서브클래스 생성 |
Spring Boot는 기본적으로 CGLIB을 사용합니다(spring.aop.proxy-target-class=true).
Self-Invocation 문제
같은 클래스 내에서 메서드를 호출하면 프록시를 거치지 않아 AOP가 적용되지 않습니다.
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 내부 호출 — 프록시를 거치지 않아 @Transactional 미적용!
this.validateOrder();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateOrder() {
// 별도 트랜잭션으로 실행되지 않음
}
}
해결 방법:
@Service
@RequiredArgsConstructor
public class OrderService {
// 방법 1: ApplicationContext에서 자기 자신의 프록시를 가져오기
private final ApplicationContext context;
public void createOrder() {
OrderService proxy = context.getBean(OrderService.class);
proxy.validateOrder(); // 프록시를 통한 호출 → AOP 적용
}
// 방법 2: 클래스를 분리하여 외부 호출로 만들기 (권장)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void validateOrder() { }
}
실전 예제: 성능 측정 AOP
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MeasureTime {}
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Around("@annotation(com.example.annotation.MeasureTime)")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
log.info("[성능 측정] {}.{} - {}ms",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
stopWatch.getTotalTimeMillis());
return result;
}
}
@Service
public class UserService {
@MeasureTime
public List<User> findAllUsers() {
// 이 메서드의 실행 시간이 자동으로 로깅됨
return userRepository.findAll();
}
}
정리
| 개념 | 설명 |
|---|---|
| Aspect | 횡단 관심사를 모듈화한 클래스 |
| Advice | 실제 부가 기능 코드 (Before/After/Around 등) |
| Pointcut | Advice 적용 위치를 정의하는 표현식 |
| JoinPoint | Advice가 실행되는 실제 시점 (메서드 실행) |
| Weaving | Aspect를 타겟 객체에 적용하는 과정 |