개발을 하다보면 로깅과 같이 같은 코드가 각각의 메소드에 똑같이 필요로 하는 경우가 있다. 이럴 경우 코드의 중복성을 줄이고 같은 코드를 재사용하기 위해 따로 관리하여 실행하는 방법이다.
간단히 설명하자면 Spring에는 프록시라는 객체는 안에 있는 Advisor를 참고하여 동작을 가로챈다. 자세한 내용은 마지막에 다시 한번 살펴보자.
Pointcut : 동작을 가로챌 타겟을 지정하는 것
다양한 (내장)Pointcut 종류 - @Transactional - @Async - @Cacheable,@CacheEvict,@CachePut - @PreAuthorize, @Secured - @Validated - @Retryable (Spring Retry) - @EventListener + @TransactionalEventListener
Advice : 동작의 실제 부가 기능을 담고 있는 것
ex) TransactionInterceptor, AsyncExecutionInterceptor
Advisor : Pointcut + Advice
Spring AOP는 Pointcut이라는 구분자를 통해 어디서 동작을 가로챌지 결정한다.
이 프록시 객체는 내부에 동작을 가로챌 위치 정보를 가진 Pointcut과 해당 위치에서 실행할 동적 정보를 가진 Advice를 묶어 놓은 Advisor(어드바이저) 라는 실행 명단을 리스트 형태로 가지고 있다.
하지만 개발을 하다보면 로깅과 같은 공통된 동작을 개발자가 만들어야 하는 상황이 생긴다. 그럴 때 사용하는 것이 @Aspect이다. 이 어노테이션은 이 클래스에 정의 된 Advisor(Pointcut + Advice)를 프록시 객체에 추가해 주라는 뜻이다.
한 마디로 Spring이 @Aspect를 파싱하여 안에 정의된 Advisor를 구성한다
다음 예시에서 살펴보자.
@Component
@Aspect
@Slf4j
public class LogAspect {
@Around( "execution(* com.example.my_api_server.service..*(..))" ) // ← [Pointcut]
public Object logging(ProceedingJoinPoint joinPoint) { // ┐
long startTime = System.currentTimeMillis(); // │
try { // │
return joinPoint.proceed(); // │ [Advice]
} catch (Throwable e) { // │
throw new RuntimeException(e); // │
} finally { // │
log.info(...); // │
} // ┘
}
}
결론 부터 말하면 @Around("execution(* com.example.my_api_server.service..*(..))")는 Pointcut 이고
public Object logging(ProceedingJoinPoint joinPoint) { ... }의 메소드가 Advice가 되어 이 둘을 합쳐서 Advisor라고 한다.
그리고 @Aspect가 붙은 클래스에 이런 Advisor를 여러개 정의할 수 있고 이 클래스는 @Component가 붙어 있기 때문에 서버가 실행될 때 프록시 객체에 이런 Advisor가 추가된다.
위에서 살펴봤던 @Transactional, @Async등의 예시는 스프링에 내장된 어노테이션으로 @Aspect에서 사용되는 포인트컷과 사용 방법에 다음과 같은 약간의 차이가 있다.
| 구분 | @Around, @Before, @After 등 | @Transactional, @Async 등 |
|---|---|---|
| 포인트컷 지정 | execution(* ...) 등으로 직접 정의 | 어노테이션을 붙이는 곳이 곧 포인트컷 |
| 로직(Advice) | 개발자가 직접 작성 | 스프링 내부에 이미 작성되어 있음 |
메서드 말고도 다양한 대상을 설정할 수 있다. 다음의 표로 참고만 하고 구체적인 문법은 따로 찾아보면 좋을 것 같다.
| 설정 방식 | 문법 (Pointcut) | 핵심 특징 | 추천 상황 |
|---|---|---|---|
| 경로 기반 | execution( com.svc...*(..)) | 특정 패키지나 메서드 패턴을 정교하게 조준 | 패키지 전체에 일괄 적용할 때 |
| 타입 기반 | within(com.svc.UserService) | 특정 클래스 내부의 모든 메서드를 조준 | 특정 서비스 전체를 감시할 때 |
| 어노테이션 기반 | @annotation(MyLog) | 특정 어노테이션이 붙은 메서드만 조준 | 원하는 곳만 콕 집어서 적용할 때 |
| bean 이름 기반 | bean(userService) | 특정 스프링 빈의 모든 메서드를 조준 | 특정 객체 단위로 관리할 때 |
사용자 정의로 가로채진 대상들에 대한 정보를 JoinPoint와 ProceedingJoinPoint를 통해 확인할 수 있다.
우리가 @Before나 @After 같은 어드바이스를 사용할 때 파라미터로 JoinPoint를 받을 수 있다. 포인트컷(execution 등)에 걸려 사냥당한 메서드의 모든 정보가 이 안에 담겨 있다.
@Before("execution(* com.example.my_api_server.service.UserService.login(..))")
public void logBefore(JoinPoint joinPoint) {
// 1. 가로챈 대상의 메서드 이름 확인
String methodName = joinPoint.getSignature().getName();
// 2. 전달된 파라미터(인자값) 확인
Object[] args = joinPoint.getArgs();
log.info("[Before Advice] 메서드명: {}, 파라미터: {}", methodName, args[0]);
// 여기서 로직이 끝나면 자동으로 실제 login() 메서드가 실행됩니다.
}
특별히 @Around 어드바이스에서는 JoinPoint를 확장한 ProceedingJoinPoint를 사용한다. @Around는 메서드의 실행 전과 후를 모두 통제하기 때문에 가로챈 동작을 언제 다시 실행할지 결정할 권한이 필요하기 때문이다.
@Around("execution(* com.example.my_api_server.service..*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
log.info("[Around Advice] 가로채기 시작: {}", joinPoint.getSignature().toShortString());
try {
// proceed()해야 실제 서비스 로직이 실행!
Object result = joinPoint.proceed();
return result; // 실행 결과를 다시 돌려줘야 흐름이 끊기지 않는다.
} finally {
long endTime = System.currentTimeMillis();
log.info("[Around Advice] 가로채기 종료 소요 시간: {}ms", (endTime - startTime));
}
}
내장 AOP의 경우에는 각각의 어노테이션마다 할 동작이 이미 정의 되어 있다. @Transactional: "가로채서 트랜잭션 열고 닫기"
@Async: "가로채서 새 쓰레드에 던지기"등 동작 로직(Advice)이 이미 고정되어 있어 개발자가 직접 다룰 필요가 없기 때문에 노출하지 않는다.
초반에 Spring에는 프록시라는 객체는 안에 있는 Advisor를 참고하여 동작을 가로챈다.라고 가볍게 설명했지만 구체적인 동작음 다음과 같다.
서버 시작 시
package com.example.my_api_server.service;
@Service
public class UserService {
@Transactional
public void login() { ... } // ← Pointcut에 매칭됨
public void signUp() { ... } // ← Pointcut에 매칭 안 됨
}
execution(* com.example.my_api_server.service.UserService.login(..))
┌─────────────────────────────────────────────┐
│ UserService 프록시 객체 │
│ │
│ ┌─ Advisor 목록 ─────────────────────────┐ │
│ │ [1] Pointcut: login() 매칭 │ │
│ │ Advice: LogAspect.logging() │ │
│ │ [2] Pointcut: @Transactional 매칭 │ │
│ │ Advice: TransactionInterceptor │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌─ 실제 객체 ────────────────────────────┐ │
│ │ UserService (원본) │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Controller가 userService.login() 호출
│
▼
프록시 객체가 대신 받음
│
├─ Advisor 목록을 순회
│ ├─ "login()이 Pointcut에 매칭되나?" → ✅ YES → Advice 실행
│ └─ "login()이 @Transactional 매칭?" → ✅ YES → 트랜잭션 Advice 실행
│
▼
실제 UserService.login() 실행