1. 지시자란
Pointcut 표현식에서 "어떤 기준으로 메서드를 선별할지" 를 지정하는 키워드다.
@Pointcut("execution(* com.example.service.*.*(..))")
// ↑
// 지시자
지시자는 선별 기준에 따라 네 가지로 분류된다.

2. execution — 가장 범용적인 지시자
메서드 시그니처 전체를 기준으로 선별한다. 리턴 타입, 패키지, 클래스, 메서드 이름, 파라미터까지 세밀하게 제어할 수 있어 가장 많이 쓰인다.

문법 구조
execution( [접근제어자]? 리턴타입 [패키지.클래스.]메서드(파라미터) [throws 예외]? )
↑ 생략 가능 ↑ 필수 ↑ 생략 가능 ↑ 필수 ↑ 생략 가능
필수 요소는 리턴 타입과 메서드(파라미터) 두 가지뿐이고 나머지는 생략 가능하다.
와일드카드
| 기호 | 의미 |
* |
임의의 한 단어 |
.. |
0개 이상 (패키지 깊이 또는 파라미터 개수) |
패키지 깊이 — .* vs ..*
// service 패키지 직속 클래스만
execution(* com.example.service.*.*(..))
// com.example.service.UserService.save() ← 적용
// com.example.service.user.UserService.save() ← 미적용 (한 단계 더 깊음)
// service 패키지 + 모든 하위 패키지
execution(* com.example.service..*.*(..))
// com.example.service.UserService.save() ← 적용
// com.example.service.user.UserService.save() ← 적용
패키지 구조가 깊어질 가능성이 있다면 ..를 쓰는 편이 안전하다.
실전 예시
// 모든 메서드 (가장 넓음, 거의 쓰지 않음)
execution(* *(..))
// 리턴 타입이 String인 메서드
execution(String com.example.service.*.*(..))
// 메서드 이름이 find로 시작
execution(* com.example.service.*.find*(..))
// 첫 번째 파라미터가 String, 나머지는 무관
execution(* com.example.service.*.*(String, ..))
// 특정 클래스의 특정 메서드 (가장 좁음)
execution(* com.example.service.UserService.findById(Long))
3. within — 타입 단위로 한번에 지정
클래스 또는 패키지 단위로 그 안의 모든 메서드에 적용한다. 메서드 시그니처는 신경 쓰지 않아서 execution보다 표현이 간결하다.
// UserService 클래스의 모든 메서드
@Pointcut("within(com.example.service.UserService)")
// service 패키지 직속 클래스의 모든 메서드
@Pointcut("within(com.example.service.*)")
// service 패키지 + 하위 패키지의 모든 메서드 (실무에서 가장 많이 쓰는 형태)
@Pointcut("within(com.example.service..*)")
4. execution vs within
두 지시자는 상속 관계와 인터페이스 지정 시 동작이 달라진다.
상속 관계에서 차이
public class UserService {
public void save() { ... }
}
public class AdminService extends UserService {
// save() 상속받음, 오버라이드 없음
public void delete() { ... }
}
// execution — 메서드가 선언된 클래스 기준
@Pointcut("execution(* com.example.service.UserService.*(..))")
// UserService.save() ← 적용 (UserService에 선언된 메서드)
// AdminService.delete() ← 미적용 (UserService에 없음)
// AdminService에서 호출한 save() ← 미적용 (선언 위치가 UserService)
// within — 실행 시점의 클래스 기준
@Pointcut("within(com.example.service.*)")
// UserService.save() ← 적용
// AdminService.delete() ← 적용 (AdminService도 service 패키지 안)
// AdminService에서 호출한 save() ← 적용 (실행 클래스가 AdminService)
인터페이스 지정 시 차이
public interface OrderService { void order(); }
@Service
public class OrderServiceImpl implements OrderService {
public void order() { ... }
}
// execution — 인터페이스 지정해도 구현체에 적용됨
@Pointcut("execution(* com.example.service.OrderService.*(..))")
// → OrderServiceImpl.order() 적용
// within — 인터페이스 지정 시 실제로 잘 안 걸림
@Pointcut("within(com.example.service.OrderService)")
// → 실행은 OrderServiceImpl에서 일어나므로 미적용
선택 기준
메서드 이름 패턴, 파라미터, 리턴 타입으로 세밀하게 제어해야 한다 → execution
특정 레이어 전체에 한번에 적용하면 된다 → within (더 간결)
5. this vs target — 프록시 vs 실제 객체
this와 target은 어느 쪽의 타입을 기준으로 선별할지의 차이다.
this → 프록시 타입 기준 ("프록시가 이 타입인가?")
target → 실제 객체 타입 기준 ("실제 객체가 이 타입인가?")

CGLIB 환경 (Spring Boot 기본)
CGLIB 프록시는 원본 클래스를 상속하므로 둘 다 구체 클래스 지정이 가능하다.
@Pointcut("this(com.example.service.OrderServiceImpl)") // 프록시가 Impl을 상속 → 적용
@Pointcut("target(com.example.service.OrderServiceImpl)") // 실제 객체가 Impl → 적용
JDK Dynamic Proxy 환경
JDK 프록시는 인터페이스 기반이라 구체 클래스를 상속하지 않는다.
@Pointcut("this(com.example.service.OrderServiceImpl)") // 프록시가 Impl 상속 안 함 → 미적용
@Pointcut("target(com.example.service.OrderServiceImpl)") // 실제 객체는 Impl → 적용
this가 유용한 경우
Spring Data JPA처럼 인터페이스만 선언하면 Spring이 프록시를 만들어주는 구조에서는 target으로 잡기 어렵고 this가 필요하다. target의 실제 구현체는 SimpleJpaRepository이므로 우리가 선언한 인터페이스 타입과 다르기 때문이다.
public interface UserRepository extends JpaRepository<User, Long> { }
// this로 프록시 타입을 직접 지정
@Pointcut("this(org.springframework.data.repository.Repository)")
실무에서는 대부분
target을 사용한다.this는 Spring Data JPA처럼 프록시만 존재하는 특수한 상황에서 의미가 있다.
6. 어노테이션 기반 지시자
어노테이션이 붙은 대상을 기준으로 선별한다. 네 가지 지시자가 있으며 어노테이션이 붙는 위치가 각각 다르다.
| 지시자 | 어노테이션 위치 | 기준 |
@annotation |
메서드 | 해당 메서드에 어노테이션이 있는가 |
@within |
클래스 | 어노테이션이 선언된 클래스 안의 메서드인가 |
@target |
클래스 | 런타임 실제 객체의 클래스에 어노테이션이 있는가 |
@args |
파라미터 타입 | 전달된 인자의 타입에 어노테이션이 있는가 |
@annotation — 가장 많이 쓰임
메서드 레벨에 어노테이션을 붙여 메서드 단위로 명시적으로 AOP를 적용한다. 범위가 명확하고 의도가 직관적이라 실무에서 가장 자주 쓰인다.
// 1. 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {}
// 2. 적용할 메서드에 붙임
@Service
public class UserService {
@Audited
public void deleteUser(Long id) { ... } // AOP 적용
public void findUser(Long id) { ... } // AOP 미적용 — 어노테이션 없으므로
}
// 3. Pointcut 선언
@Pointcut("@annotation(com.example.annotation.Audited)")
public void auditedMethods() {}
@within vs @target — 상속 관계에서의 차이

둘 다 클래스 레벨 어노테이션을 대상으로 하지만 상속 관계에서 동작이 다르다.
@Loggable
public class UserService {
public void save() { ... }
}
public class AdminService extends UserService {
// @Loggable 없음, save()는 상속
public void delete() { ... }
}
// @within: 어노테이션이 선언된 클래스 기준
@Pointcut("@within(com.example.annotation.Loggable)")
// UserService.save() ← 적용 (선언 클래스 UserService에 @Loggable)
// AdminService.delete() ← 미적용 (선언 클래스 AdminService에 @Loggable 없음)
// AdminService에서 호출한 save() ← 미적용 (실행 클래스가 AdminService이므로)
// @target: 런타임 실제 객체 기준
@Pointcut("@target(com.example.annotation.Loggable)")
// UserService 인스턴스.save() ← 적용 (실제 객체 UserService에 @Loggable)
// AdminService 인스턴스.delete() ← 미적용 (실제 객체 AdminService에 @Loggable 없음)
// AdminService 인스턴스에서 save() ← 미적용 (실제 객체가 AdminService)
위 예시에서는 결과가 동일하다. 차이가 드러나는 경우는 @Loggable을 가진 클래스의 인스턴스를 직접 사용할 때다.
// UserService 인스턴스를 직접 참조하는 경우
UserService svc = new UserService(); // @Loggable 있음
svc.save();
// @within → 선언 클래스(UserService)에 @Loggable → 적용
// @target → 런타임 객체(UserService 인스턴스)에 @Loggable → 적용
// 여기선 동일
// AdminService를 UserService 타입으로 업캐스팅하는 경우
UserService svc = new AdminService(); // 실제 객체는 AdminService
svc.save();
// @within → 메서드 선언이 UserService → 적용
// @target → 런타임 실제 객체가 AdminService → 미적용 ← 여기서 차이
@within은 컴파일 시점 선언 위치를, @target은 런타임 실제 인스턴스를 본다. 실무에서는 @within이 더 예측 가능하고 일반적이다.
7. args / bean
args — 런타임 파라미터 타입으로 선별
전달된 런타임 인자의 실제 타입을 기준으로 선별한다.
@Pointcut("args(String)") // 파라미터가 정확히 String 하나
@Pointcut("args(Long)") // 파라미터가 정확히 Long 하나
@Pointcut("args(String, ..)") // 첫 번째가 String, 나머지는 무관
execution의 파라미터 지정과 비슷해 보이지만 판단 시점이 다르다.
execution(* *(String)) // 컴파일 시점 — 메서드 선언에 String이 적혀있는가
args(String) // 런타임 시점 — 실제로 전달된 인자가 String인가
예를 들어 Object 타입으로 선언된 파라미터에 String을 전달하면 execution은 잡지 못하지만 args는 잡는다. 단독으로 쓰기보다 execution과 조합해서 쓰는 경우가 많다.
bean — Spring Bean 이름으로 선별
Spring AOP 전용 지시자로, Bean 이름을 기준으로 선별한다. AspectJ에는 없다.
@Pointcut("bean(userService)") // userService Bean의 모든 메서드
@Pointcut("bean(*Service)") // 이름이 Service로 끝나는 모든 Bean
특정 Bean만 빠르게 지정하고 싶을 때 유용하지만, 패키지 기반 within이 더 구조적이라 실무에서는 드물게 쓰인다.
8. 지시자 조합
&&, ||, ! 연산자로 여러 지시자를 조합할 수 있다. Pointcut을 메서드로 미리 선언해두면 재사용하기 편하다.
@Aspect
@Component
public class AuditAspect {
// 재사용할 Pointcut을 메서드로 분리
@Pointcut("within(com.example.service..*)")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.annotation.Audited)")
public void audited() {}
// service 레이어 안에서 @Audited가 붙은 메서드만
@Pointcut("serviceLayer() && audited()")
public void auditTarget() {}
@Around("auditTarget()")
public Object audit(ProceedingJoinPoint pjp) throws Throwable { ... }
}
인라인으로도 쓸 수 있다.
// 메서드 분리 없이 바로 조합
@Around("within(com.example.service..*) && @annotation(com.example.annotation.Audited)")
public Object audit(ProceedingJoinPoint pjp) throws Throwable { ... }
조합이 단순하면 인라인, 여러 곳에서 재사용하거나 조건이 복잡하면 메서드로 분리하는 것이 일반적이다.
9. 정리
| 지시자 | 기준 | 주 용도 |
execution |
메서드 시그니처 | 세밀한 메서드 제어 |
within |
클래스/패키지 타입 | 레이어 전체 적용 |
this |
프록시 타입 | Spring Data JPA 등 특수 케이스 |
target |
실제 객체 타입 | 구현 타입 필터링 (일반적) |
@annotation |
메서드 레벨 어노테이션 | 메서드 단위 명시적 적용 |
@within |
선언 클래스 레벨 어노테이션 | 클래스 전체 적용 |
@target |
런타임 객체 클래스 어노테이션 | 런타임 타입 기반 적용 |
@args |
파라미터 타입 어노테이션 | 특정 타입 인자 감지 |
args |
런타임 파라미터 타입 | 파라미터 타입 필터링 |
bean |
Spring Bean 이름 | Bean 이름 기반 빠른 지정 |
다음 포스트에서는 Advice 종류(@Before, @After, @Around 등)와 실행 순서, 실전 패턴을 다룬다.
'Spring Framework > Spring' 카테고리의 다른 글
| [Spring] Spring AOP — Advice 종류와 실행 순서 (0) | 2026.06.01 |
|---|---|
| [Spring] Spring AOP — 개념과 구조 (0) | 2026.06.01 |
| [Spring] 트랜잭션 완전 정리 - @Transactional의 내부 동작과 함정들 (0) | 2026.05.31 |
| [Spring] Spring은 DB 예외를 어떻게 바꾸는가 (0) | 2026.05.27 |
| [Spring] Spring의 비동기 처리 — @Async부터 Virtual Thread 통합까지 (0) | 2026.03.16 |