Spring AOP 핵심 정리: 사용자 정의 AOP와 내장 AOP의 차이

신지훈·2026년 3월 18일

JAVA/Spring/JVM

목록 보기
10/14

AOP란?

개발을 하다보면 로깅과 같이 같은 코드가 각각의 메소드에 똑같이 필요로 하는 경우가 있다. 이럴 경우 코드의 중복성을 줄이고 같은 코드를 재사용하기 위해 따로 관리하여 실행하는 방법이다.

동작 원리

간단히 설명하자면 Spring에는 프록시라는 객체는 안에 있는 Advisor를 참고하여 동작을 가로챈다. 자세한 내용은 마지막에 다시 한번 살펴보자.

용어 정리 : Pointcut, Advice, Advisor

Pointcut : 동작을 가로챌 타겟을 지정하는 것

다양한 (내장)Pointcut 종류
- @Transactional
- @Async
- @Cacheable,@CacheEvict,@CachePut
- @PreAuthorize, @Secured
- @Validated
- @Retryable (Spring Retry)
- @EventListener + @TransactionalEventListener

Advice : 동작의 실제 부가 기능을 담고 있는 것

ex) TransactionInterceptor, AsyncExecutionInterceptor

Advisor : Pointcut + Advice

사용자 정의 AOP

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가 추가된다.

사용자 정의 AOP vs 스프링 내장 어노테이션

위에서 살펴봤던 @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

사용자 정의로 가로채진 대상들에 대한 정보를 JoinPoint와 ProceedingJoinPoint를 통해 확인할 수 있다.

1. JoinPoint

우리가 @Before나 @After 같은 어드바이스를 사용할 때 파라미터로 JoinPoint를 받을 수 있다. 포인트컷(execution 등)에 걸려 사냥당한 메서드의 모든 정보가 이 안에 담겨 있다.

  • 포함된 정보: 메서드 이름, 리턴 타입, 전달된 파라미터(Args), 실제 호출된 객체(Target) 등.
  • 특징: 단순한 읽기 전용 정보로 가로챈 시점에 정보를 확인하고 기록하는 용도로 사용한다.
@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() 메서드가 실행됩니다.
}

2. ProceedingJoinPoint

특별히 @Around 어드바이스에서는 JoinPoint를 확장한 ProceedingJoinPoint를 사용한다. @Around는 메서드의 실행 전과 후를 모두 통제하기 때문에 가로챈 동작을 언제 다시 실행할지 결정할 권한이 필요하기 때문이다.

  • 핵심 기능 (proceed() 메서드): 이 메서드를 호출하는 시점이 바로 가로채서 멈춰두었던 실제 비즈니스 로직이 실행되는 시점이다.
  • 통제권: 만약 proceed()를 호출하지 않는다면? 실제 메서드는 영원히 실행되지 않는다.
@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에는 JoinPoint 같은게 없나?

내장 AOP의 경우에는 각각의 어노테이션마다 할 동작이 이미 정의 되어 있다. @Transactional: "가로채서 트랜잭션 열고 닫기"
@Async: "가로채서 새 쓰레드에 던지기"등 동작 로직(Advice)이 이미 고정되어 있어 개발자가 직접 다룰 필요가 없기 때문에 노출하지 않는다.

동작 최종 정리

초반에 Spring에는 프록시라는 객체는 안에 있는 Advisor를 참고하여 동작을 가로챈다.라고 가볍게 설명했지만 구체적인 동작음 다음과 같다.

서버 시작 시

  1. Spring이 @Aspect 클래스를 스캔
  2. Pointcut 표현식을 파싱 → "UserService의 login()이 대상이구나"
  3. UserService 빈을 생성할 때 프록시 객체로 감싸서 등록
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() 실행
profile
주주주주니어 개발자

0개의 댓글