Spring AOP 사용법: @Aspect와 Pointcut 표현식

Spring AOP 동작 원리 - 프록시부터 CGLIB까지 글에서는 Spring AOP가 프록시로 동작한다는 사실을 정리했습니다. 이번 글에서는 Spring AOP에서 부가 기능을 어떻게 작성하고 어디에 적용하는지, 즉
@Aspect문법을 정리합니다.
Spring AOP는 AspectJ의 어노테이션과 포인트컷 표현식을 빌려 씁니다. 문법만 빌려 쓸 뿐 실제로는 프록시로 동작하므로, AspectJ 컴파일러나 Java Agent를 따로 설정하지 않았다면 바이트코드를 수정하는 작업(Weaving)은 일어나지 않습니다.
@Aspect, Advice, Pointcut 한눈에 보기
| 구성 | 역할 | 예시 |
|---|---|---|
@Aspect | 이 클래스가 부가 기능 모듈(Aspect)이라는 것을 표시합니다 | @Aspect @Component |
| Advice 어노테이션 | 부가 기능을 언제 실행할지 정합니다 | @Around, @Before 등 |
| Pointcut 표현식 | 부가 기능을 어디에 적용할지 정합니다 | execution(* com.shop..*(..)) |
@Aspect
@Component // @Aspect만으로는 빈 등록이 안 되므로 함께 붙인다
public class TimeAspect {
@Around("execution(* com.shop.service..*(..))")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 진짜 메서드 실행
log.info("{} took {}ms", joinPoint.getSignature(), System.currentTimeMillis() - start);
return result;
}
}@Around는 메서드 실행 전후를 모두 감쌉니다. (언제)execution(* com.shop.service..*(..))는service패키지와 그 하위 패키지에 있는 모든 메서드에 부가 기능을 적용합니다. (어디에)joinPoint.proceed()를 호출해야 진짜 메서드가 실행됩니다.
Advice 종류: 언제 실행할까
| 어노테이션 | 실행 시점 |
|---|---|
@Before | 메서드가 실행되기 전 |
@AfterReturning | 메서드가 정상적으로 종료된 후 (반환값에 접근할 수 있음) |
@AfterThrowing | 메서드에서 예외가 발생한 후 |
@After | 정상 종료든 예외든 항상 실행된 후 (finally와 비슷함) |
@Around | 실행 전후를 모두 제어함. proceed()를 직접 호출해야 진짜 메서드가 실행됨 |
@Before("execution(* com.shop.service..*(..))")
public void before(JoinPoint joinPoint) {
log.info("call: {}", joinPoint.getSignature());
}
@AfterReturning(pointcut = "execution(* com.shop.service..*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("return: {}", result);
}
@AfterThrowing(pointcut = "execution(* com.shop.service..*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
log.error("exception: {}", ex.getMessage());
}@Around가 가장 강력합니다. 진짜 메서드를 실행할지, 어떤 인자를 넘길지, 반환값과 예외를 어떻게 처리할지까지 모두 제어할 수 있습니다.@Transactional도 이처럼 메서드 실행 전후를 감싸는 방식으로 동작합니다.- 대신
proceed()호출을 빠뜨리면 진짜 메서드가 실행되지 않으므로 주의해야 합니다. - 필요한 만큼만 사용하는 것이 좋습니다. 실행 전에 로그만 남기면 된다면
@Before로 충분합니다.
JoinPoint와 ProceedingJoinPoint
| 사용하는 Advice | 제공하는 기능 | |
|---|---|---|
JoinPoint | @Before, @After, @AfterReturning, @AfterThrowing | 메서드 정보(getSignature()), 인자(getArgs()), 대상 객체(getTarget()) |
ProceedingJoinPoint | @Around | JoinPoint의 기능 + proceed()로 진짜 메서드 실행 |
Pointcut 표현식: 어디에 적용할까
1. execution: 메서드 시그니처로 지정하기 (가장 많이 사용함)
execution( [접근제어자] 반환타입 [패키지.클래스.]메서드이름(파라미터) )
[ ]로 표시한 부분은 생략할 수 있습니다.
| 표현식 | 의미 |
|---|---|
execution(* com.shop.service.OrderService.order(..)) | OrderService의 order 메서드 |
execution(* com.shop.service.*.*(..)) | service 패키지에 있는 모든 클래스의 모든 메서드 (하위 패키지는 제외) |
execution(* com.shop..*(..)) | com.shop과 그 하위 패키지 전체에 있는 모든 메서드 |
execution(public String com.shop..find*(Long)) | 이름이 find로 시작하고, Long 하나를 받고, String을 반환하는 public 메서드 |
| 기호 | 위치 | 의미 |
|---|---|---|
* | 어디든 | 아무 값 하나 (반환 타입, 클래스 이름, 메서드 이름 등) |
.. | 패키지 | 하위 패키지를 포함함 |
.. | 파라미터 | 파라미터 개수와 타입에 관계없음 |
2. @annotation: 특정 어노테이션이 붙은 메서드에 적용하기 (실무에서 가장 편함)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}@Aspect
@Component
public class TimeAspect {
@Around("@annotation(com.shop.aop.LogExecutionTime)")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
log.info("{} took {}ms", joinPoint.getSignature(), System.currentTimeMillis() - start);
return result;
}
}@Service
public class OrderService {
@LogExecutionTime // 적용하고 싶은 메서드에 붙이기만 하면 된다
public void order() { ... }
}이 방식을 사용하면 패키지 구조와 관계없이 원하는 메서드에만 명시적으로 부가 기능을 적용할 수 있습니다. 또한 코드만 보고도 어떤 부가 기능이 적용되는지 알 수 있다는 장점이 있습니다.
3. 그 밖의 표현식
| 표현식 | 의미 |
|---|---|
within(com.shop.service..*) | 해당 패키지 안에 있는 타입에 속한 모든 메서드 |
@within(org.springframework.stereotype.Service) | 해당 어노테이션이 붙은 클래스의 모든 메서드 |
args(Long, ..) | 첫 번째 인자가 Long인 메서드 |
bean(orderService) | 해당 이름으로 등록된 빈 (Spring AOP 전용이며 AspectJ에는 없음) |
bean()은 Spring AOP가 추가한 표현식입니다. 이 사실은 Spring AOP가 AspectJ 문법을 그대로 사용하는 것이 아니라, 빌려 쓰면서 필요한 기능을 덧붙였다는 점을 보여 줍니다.
Pointcut 재사용과 조합
같은 표현식을 여러 Advice에서 사용한다면 @Pointcut으로 이름을 붙여서 재사용합니다.
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.shop.service..*(..))")
private void serviceLayer() {} // 포인트컷에 이름 붙이기
@Pointcut("@annotation(com.shop.aop.NoLog)")
private void noLog() {}
@Before("serviceLayer() && !noLog()") // &&, ||, ! 로 조합
public void log(JoinPoint joinPoint) {
log.info("call: {}", joinPoint.getSignature());
}
}@Pointcut메서드의 본문은 비워 둡니다. 이 메서드는 포인트컷에 이름을 붙이는 역할만 합니다.- 포인트컷은
&&,||,!로 조합할 수 있습니다. 위 예시는 “서비스 계층의 메서드 중에서@NoLog가 붙지 않은 메서드”를 뜻합니다.
여러 Aspect의 실행 순서
같은 메서드에 Aspect가 여러 개 적용되면 실행 순서가 보장되지 않습니다. 순서가 중요하다면 @Order로 지정합니다. 숫자가 작을수록 바깥쪽에서 먼저 실행됩니다.
@Aspect
@Component
@Order(1) // 먼저 실행 (바깥쪽)
public class AuthAspect { ... }
@Aspect
@Component
@Order(2) // 나중에 실행 (안쪽)
public class TimeAspect { ... }AuthAspect 시작 → TimeAspect 시작 → 진짜 메서드 → TimeAspect 끝 → AuthAspect 끝
@Order는 Aspect 클래스 단위로만 적용됩니다. 한 Aspect 클래스 안에 있는 여러 Advice 사이의 순서를 정하려면 Aspect 클래스를 나눠야 합니다.
정리
| 구분 | 핵심 |
|---|---|
@Aspect + @Component | 부가 기능 모듈을 정의하고 빈으로 등록합니다 |
| Advice | 언제 실행할지 정합니다: @Before, @AfterReturning, @AfterThrowing, @After, @Around |
| Pointcut | 어디에 적용할지 정합니다: execution(시그니처), @annotation(어노테이션), within, args, bean |
@Pointcut | 표현식에 이름을 붙여 재사용하고, &&, ||, !로 조합합니다 |
@Order | 여러 Aspect의 실행 순서를 정합니다. 숫자가 작을수록 바깥쪽에서 실행됩니다 |
이 문법으로 작성한 Aspect도 결국 프록시로 적용됩니다. 따라서 이전 글에서 정리한 **프록시 방식의 한계(내부 호출, private·final 메서드)**가 그대로 적용됩니다.