1. Advice란
메서드 실행의 어느 시점에 끼워 넣을 부가 기능 로직이다. 적용 시점에 따라 다섯 가지 종류가 있다.

| 종류 | 시점 |
@Before |
메서드 실행 전 |
@After |
메서드 실행 후 (정상/예외 무관) |
@AfterReturning |
정상 반환 후 |
@AfterThrowing |
예외 발생 후 |
@Around |
전후 모두 감쌈 (가장 강력) |
2. @Before
메서드 실행 전에 동작한다. 메서드 호출을 막거나 파라미터를 바꿀 수는 없다. 그런 제어가 필요하다면 @Around를 써야 한다.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
log.info("호출: {}", jp.getSignature().toShortString());
}
}
@Before의 반환 타입은 void여야 한다. 반환값을 지정해도 AOP 프레임워크가 무시한다.
3. @After / @AfterReturning / @AfterThrowing
세 Advice는 모두 메서드 실행 이후 시점에 동작하며, 정상/예외 중 어느 경우에 실행되는지가 다르다.
@After — 예외 무관하게 항상 실행
메서드가 정상 반환하든 예외가 터지든 항상 실행된다. Java의 finally와 같은 역할이다. 반환값이나 예외 객체를 받는 속성은 없다.
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint jp) {
log.info("종료: {}", jp.getSignature().toShortString());
}
@AfterReturning — 정상 반환 후
메서드가 정상적으로 반환된 후에만 실행된다. returning 속성으로 반환값을 받아볼 수 있다.
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result" // 반환값을 result 파라미터로 받음
)
public void logReturn(JoinPoint jp, Object result) {
log.info("반환값: {}", result);
}
반환값을 읽을 수는 있지만 바꿀 수는 없다. 반환값을 바꿔야 한다면 @Around를 써야 한다.
@AfterThrowing — 예외 발생 후
메서드에서 예외가 발생했을 때만 실행된다. throwing 속성으로 예외 객체를 받을 수 있다.
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex" // 예외 객체를 ex 파라미터로 받음
)
public void logException(JoinPoint jp, Exception ex) {
log.error("예외 발생 — {}: {}", jp.getSignature().toShortString(), ex.getMessage());
}
예외를 잡아서 삼킬 수는 없다. @AfterThrowing이 실행된 후 예외는 그대로 전파된다. 예외를 처리하거나 변환해야 한다면 @Around를 써야 한다.
4. @Around — 가장 강력한 Advice
메서드 실행 전후를 모두 감싸며, pjp.proceed()로 실제 메서드 호출 시점을 직접 제어한다.
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
// 전처리
log.info("시작: {}", pjp.getSignature().toShortString());
long start = System.currentTimeMillis();
// 실제 메서드 호출 — 반드시 호출해야 함
Object result = pjp.proceed();
// 후처리
long elapsed = System.currentTimeMillis() - start;
log.info("완료: {} ({}ms)", pjp.getSignature().toShortString(), elapsed);
return result; // 반환값을 직접 제어 가능
}

@Around만 파라미터가 JoinPoint가 아닌 ProceedingJoinPoint 다. proceed()를 호출해야 실제 메서드가 실행되므로, 실수로 빠뜨리면 메서드가 아예 실행되지 않는다.
@Around로 할 수 있는 것
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1. 실행 자체를 막을 수 있음
if (!hasPermission()) {
throw new AccessDeniedException("권한 없음");
}
Object result;
try {
result = pjp.proceed();
} catch (Exception e) {
// 2. 예외를 잡아서 처리하거나 변환할 수 있음
log.error("예외 처리", e);
throw new CustomException("처리 실패", e);
}
// 3. 반환값을 바꿀 수 있음
if (result instanceof String str) {
return str.trim();
}
return result;
}
언제 @Around를 쓸까
반환값을 바꿔야 한다 → @Around
예외를 잡아서 처리/변환해야 한다 → @Around
실행 시간을 측정해야 한다 → @Around
단순히 실행 전/후 로그만 찍는다 → @Before / @After 로 충분
@Around는 강력하지만 proceed() 호출을 잊거나 반환값을 빠뜨리는 실수가 생길 수 있다. 굳이 필요하지 않다면 의도에 맞는 구체적인 Advice를 쓰는 편이 낫다.
5. JoinPoint와 ProceedingJoinPoint
모든 Advice는 첫 번째 파라미터로 JoinPoint를 받을 수 있다. @Around는 ProceedingJoinPoint를 받는다.
// JoinPoint 주요 메서드
jp.getSignature() // 메서드 시그니처 (이름, 리턴타입 등)
jp.getSignature().getName() // 메서드 이름만
jp.getArgs() // 전달된 파라미터 배열 (Object[])
jp.getTarget() // 실제 객체 (target)
jp.getThis() // 프록시 객체 (this)
// ProceedingJoinPoint 추가 메서드 (@Around 전용)
pjp.proceed() // 실제 메서드 호출
pjp.proceed(newArgs) // 파라미터를 바꿔서 호출
6. 파라미터 바인딩
JoinPoint.getArgs()로 파라미터를 꺼내면 Object[] 배열이라 타입 캐스팅이 필요하다. Pointcut 표현식에서 직접 파라미터를 바인딩하면 타입 안전하게 받을 수 있다.
args() 바인딩
// getArgs() 방식 — Object[] 배열, 타입 캐스팅 필요
@Before("execution(* com.example.service.UserService.save(..))")
public void beforeV1(JoinPoint jp) {
User user = (User) jp.getArgs()[0];
log.info("저장 요청: {}", user.getName());
}
// args() 바인딩 방식 — 타입 안전하게 직접 받음
@Before("execution(* com.example.service.UserService.save(..)) && args(user)")
public void beforeV2(User user) {
log.info("저장 요청: {}", user.getName());
}
Pointcut 표현식의 args(user)에서 user는 파라미터 이름이다. Advice 메서드의 파라미터 이름과 일치해야 바인딩된다.
파라미터가 여러 개일 때도 동일하다.
@Before("execution(* com.example.service.*.*(..)) && args(id, name)")
public void before(Long id, String name) {
log.info("id={}, name={}", id, name);
}
@annotation() 바인딩
@annotation을 쓸 때 어노테이션 인스턴스 자체를 바인딩할 수 있다. 어노테이션에 속성이 있을 때 유용하다.
// 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int limit() default 100;
}
// 바인딩 — 표현식의 rateLimit이 파라미터 이름 RateLimit rateLimit과 매칭
@Around("@annotation(rateLimit)")
public Object checkRateLimit(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
int limit = rateLimit.limit(); // 어노테이션 속성값 직접 접근
log.info("rate limit: {}", limit);
return pjp.proceed();
}
@within() / @target() 바인딩
클래스 레벨 어노테이션도 같은 방식으로 바인딩할 수 있다.
// 어노테이션 정의
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String level() default "INFO";
}
// 바인딩
@Before("@within(loggable)")
public void before(JoinPoint jp, Loggable loggable) {
String level = loggable.level(); // 클래스에 붙은 어노테이션 속성 접근
log.info("level={}, method={}", level, jp.getSignature().getName());
}
JoinPoint와 함께 쓸 때
바인딩 파라미터와 JoinPoint를 함께 받을 수 있다. 이때 JoinPoint는 반드시 첫 번째 파라미터여야 한다.
@Before("execution(* com.example.service.UserService.save(..)) && args(user)")
public void before(JoinPoint jp, User user) { // JoinPoint 먼저
log.info("메서드: {}, 파라미터: {}", jp.getSignature().getName(), user);
}
7. 여러 Aspect가 겹칠 때 — @Order
같은 메서드에 여러 Aspect가 적용될 때 실행 순서는 @Order로 지정한다.

@Order(1) @Aspect @Component // 숫자가 낮을수록 바깥쪽 — 먼저 진입, 나중에 복귀
public class SecurityAspect { ... }
@Order(2) @Aspect @Component
public class TransactionAspect { ... }
@Order(3) @Aspect @Component
public class LoggingAspect { ... }
실행 흐름은 이렇다.
진입: SecurityAspect(1) → TransactionAspect(2) → LoggingAspect(3) → 실제 메서드
복귀: 실제 메서드 → LoggingAspect(3) → TransactionAspect(2) → SecurityAspect(1)
@Order를 지정하지 않으면 실행 순서가 비결정적이다. 여러 Aspect를 함께 쓴다면 반드시 지정해야 한다.
순서가 중요한 이유
예를 들어 보안 Aspect가 트랜잭션 Aspect보다 안쪽에서 실행되면, 인증 실패 시 이미 트랜잭션이 열린 뒤 예외가 발생한다. 보안 → 트랜잭션 → 로깅 순서로 바깥부터 진입하는 게 일반적으로 올바른 설계다.
8. 실전 패턴
실행 시간 측정
@Around("within(com.example.service..*)")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long elapsed = System.currentTimeMillis() - start;
if (elapsed > 1000) { // 1초 초과 시 경고
log.warn("느린 메서드: {} ({}ms)", pjp.getSignature().toShortString(), elapsed);
}
return result;
}
감사 로그 (Audit Log)
어노테이션 바인딩을 활용해 어노테이션 속성까지 함께 기록한다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
String action() default ""; // 감사 행위 설명
}
@AfterReturning(
pointcut = "@annotation(audited)", // 바인딩
returning = "result"
)
public void audit(JoinPoint jp, Audited audited, Object result) {
String user = SecurityContextHolder.getContext()
.getAuthentication().getName();
log.info("[AUDIT] user={}, action={}, method={}, result={}",
user, audited.action(), jp.getSignature().getName(), result);
}
예외 변환
@Around("within(com.example.repository..*)")
public Object translateException(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} catch (DataAccessException e) {
// 인프라 예외를 도메인 예외로 변환
throw new RepositoryException("데이터 접근 오류", e);
}
}
9. 정리
| 종류 | 시점 | 반환값 제어 | 예외 처리 | 주 용도 |
@Before |
실행 전 | 불가 | 불가 | 파라미터 로깅, 인증 확인 |
@After |
실행 후 (무조건) | 불가 | 불가 | 리소스 정리, 완료 로깅 |
@AfterReturning |
정상 반환 후 | 읽기만 가능 | 불가 | 반환값 로깅, 감사 |
@AfterThrowing |
예외 발생 후 | 불가 | 불가 | 예외 로깅, 알림 |
@Around |
전후 모두 | 가능 | 가능 | 실행 시간 측정, 예외 변환, 트랜잭션 |
단순 로그/확인 → 목적에 맞는 구체적인 Advice (@Before, @AfterReturning 등)
반환값 제어 → @Around
예외 처리/변환 → @Around
여러 Aspect가 겹칠 때는 @Order로 실행 순서를 반드시 명시한다.
'Spring Framework > Spring' 카테고리의 다른 글
| [Spring] Spring AOP - Pointcut 지시자 완전 정복 (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 |