[Advanced-8] @Aspect AOP

2026. 1. 2. 19:12·Spring/Core

1. @Aspect 프록시 - 소개

1.1. @Aspect의 등장: 선언적 프로그래밍의 완성

 지금까지 우리는 빈 후처리기를 통해 프록시 생성 자동화 문제를 해결했다. 하지만 여전히 어드바이저 설정 코드는 수동으로 작성해야 했다. 하지만 @Aspect의 등장으로 이 마지막 장벽도 해결할 수 있다.

발전 과정 정리:

1단계: 수동 프록시 생성 (문제: N개 빈 → N개 설정)
2단계: 프록시 팩토리 (문제: 설정 폭발)
3단계: 빈 후처리기 (해결: 자동 프록시 생성, 문제: 어드바이저 설정 필요)
4단계: @Aspect (완전 해결: 선언적 AOP)

1.2. 빈 후처리기 방식의 문제점

빈 후처리기를 사용하면 프록시 생성은 자동화되었지만, 어드바이저 설정 코드는 여전히 수동으로 작성해야 했다.

@Configuration
public class BeanPostProcessorConfig {
    
    @Bean
    public BeanPostProcessor proxyProcessor() {
        // 1. 어드바이저 생성 (수동)
        Advisor advisor = new DefaultPointcutAdvisor(
            new NameMatchMethodPointcut(),  // 포인트컷 설정
            new LogTraceAdvice(logTrace)    // 어드바이스 설정
        );
        
        // 2. 빈 후처리기 생성
        return new PackageLogTraceProxyPostProcessor("hello.proxy.app", advisor);
    }
}

문제점:

  • 포인트컷 표현식이 설정 코드에 하드코딩됨
  • 어드바이스 로직과 설정이 분리되어 있음
  • 새로운 AOP 적용 시마다 새로운 설정 클래스 필요

1.3. @Aspect 방식: 설정의 끝, 선언의 시작

@Aspect는 선언적 프로그래밍(Declarative Programming) 방식을 도입하여 모든 설정 코드를 제거했다.

// ❌ 기존 방식: 설정 코드 (빈 후처리기)
@Bean
public BeanPostProcessor proxyProcessor() {
    // 설정, 설정, 설정...
    Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
    return new PackageLogTraceProxyPostProcessor(basePackage, advisor);
}
// ✅ @Aspect 방식: 선언만 하면 끝!
@Aspect
public class LogTraceAspect {
    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        // 로직
    }
}

1.4. @Aspect의 핵심 아이디어

@Aspect는 스프링이 제공하는 애노테이션 기반 AOP 기능이다. 이는 AspectJ 프로젝트에서 차용한 것으로, 스프링은 이를 프록시 기반 AOP로 구현했다.

핵심 원리:

  1. @Aspect가 붙은 클래스를 스프링 빈으로 등록
  2. 스프링이 자동으로 이를 분석하여 어드바이저로 변환
  3. 자동 프록시 생성기가 어드바이저 기반으로 프록시 생성

1.5. LogTraceAspect 구현

package hello.proxy.config.v6_aop.aspect;

import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;

        // 디버깅용 (필요시 주석 해제)
        // log.info("target={}", joinPoint.getTarget());
        // log.info("getArgs={}", joinPoint.getArgs());
        // log.info("getSignature={}", joinPoint.getSignature());

        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            // 실제 비즈니스 로직 호출
            Object result = joinPoint.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

1.6. 코드 분석

1.6.1. @Aspect 애노테이션

@Aspect
  • 이 클래스가 AOP 애스펙트임을 선언한다
  • 스프링이 이 클래스를 AOP 처리 대상으로 인식한다

1.6.2. @Around 애노테이션

@Around("execution(* hello.proxy.app..*(..))")
  • @Around: 메서드 실행 전후에 처리할 어드바이스를 정의한다
  • execution(* hello.proxy.app..*(..)): AspectJ 포인트컷 표현식
    • : 모든 반환 타입
    • hello.proxy.app..: 해당 패키지와 모든 하위 패키지
    • (..): 모든 메서드, 모든 파라미터

1.6.3. ProceedingJoinPoint

public Object execute(ProceedingJoinPoint joinPoint)
  • ProceedingJoinPoint: 어드바이스가 적용되는 조인 포인트 정보를 담는다
  • MethodInvocation과 유사하지만 더 많은 정보를 제공한다
  • 주요 메서드:
    • joinPoint.proceed(): 실제 대상 메서드를 호출한다
    • joinPoint.getSignature(): 호출된 메서드 정보를 얻는다
    • joinPoint.getArgs(): 메서드 인자들을 얻는다
    • joinPoint.getTarget(): 실제 대상 객체를 얻는다

1.7. AopConfig 설정

package hello.proxy.config.v6_aop;

import hello.proxy.config.AppV1Config;
import hello.proxy.config.AppV2Config;
import hello.proxy.config.v6_aop.aspect.LogTraceAspect;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AopConfig {

    @Bean
    public LogTraceAspect logTraceAspect(LogTrace logTrace) {
        return new LogTraceAspect(logTrace);
    }
}

설명:

  • @Import({AppV1Config.class, AppV2Config.class}): V1, V2 애플리케이션을 수동으로 등록한다
  • @Bean logTraceAspect(): @Aspect 클래스를 스프링 빈으로 등록한다
    • @Component를 사용해 컴포넌트 스캔으로 등록해도 된다
    • 명시적으로 빈으로 등록하는 것이 의존성 관리에 더 좋다

1.8. ProxyApplication 설정

@Import(AopConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app")
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

1.9. 실행 결과

애플리케이션 로딩 로그:

# 모든 애플리케이션에 프록시 자동 적용
create proxy: target=v1.OrderRepositoryV1Impl proxy=class com.sun.proxy.$Proxy50
create proxy: target=v2.OrderRepositoryV2 proxy=class ...$$EnhancerBySpringCGLIB$$
create proxy: target=v3.OrderRepositoryV3 proxy=class ...$$EnhancerBySpringCGLIB$$

실행 테스트:

  • http://localhost:8080/v1/request?itemId=hello → 정상 동작
  • http://localhost:8080/v2/request?itemId=hello → 정상 동작
  • http://localhost:8080/v3/request?itemId=hello → 정상 동작

특징:

  • V1: 인터페이스 기반 → JDK 동적 프록시
  • V2, V3: 클래스 기반 → CGLIB 프록시
  • 컴포넌트 스캔(V3)도 자동으로 프록시 적용

2. @Aspect 프록시 - 설명

2.1. 자동 프록시 생성기의 확장된 역할

이전에 배운 AnnotationAwareAspectJAutoProxyCreator(자동 프록시 생성기)는 두 가지 역할을 수행한다:

  1. 기존 역할: Advisor를 찾아서 프록시 생성
  2. 새로운 역할: @Aspect를 찾아서 Advisor로 변환

이름에 AnnotationAware가 붙은 이유도 이 때문이다. 애노테이션(@Aspect)을 인식(aware)하고 처리한다.

2.2. @Aspect → Advisor 변환 과정

2.2.1. 변환 과정 상세

1. 스프링 애플리케이션 시작
   ↓
2. 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator) 동작
   ↓
3. 모든 @Aspect 빈 조회
   ↓
4. @Aspect 어드바이저 빌더(BeanFactoryAspectJAdvisorsBuilder) 생성
   ↓
5. AspectJ 정보 분석 → Advisor 생성
   ↓
6. 생성된 Advisor 캐시 저장

2.2.2. BeanFactoryAspectJAdvisorsBuilder

  • @Aspect 정보를 기반으로 어드바이저를 생성하는 빌더 클래스
  • 내부 캐시를 사용하여 성능 최적화
  • 동일한 @Aspect는 캐시에서 재사용
  • @Aspect의 @Around, @Before, @After 등을 분석하여 각각의 어드바이저 생성

2.3. 어드바이저 기반 프록시 생성 과정 (@Aspect 포함)

1. 스프링 빈 객체 생성 (모든 빈 대상)
   ↓
2. 빈 후처리기(자동 프록시 생성기)에 전달
   ↓
3-1. 일반 Advisor 빈들 조회 (직접 등록한 Advisor)
   ↓
3-2. @Aspect 기반 Advisor 조회 (BeanFactoryAspectJAdvisorsBuilder에서)
   ↓
4. 모든 Advisor의 포인트컷으로 프록시 적용 대상 체크
   ↓
5. 적용 대상이면 프록시 생성 (여러 Advisor 포함 가능)
   ↓
6. 프록시 또는 원본 객체 반환 → 스프링 빈으로 등록

2.4. 포인트컷의 두 단계 검증

자동 프록시 생성기는 포인트컷을 두 단계에 걸쳐 검증한다:

2.4.1. 첫 번째 단계: 프록시 생성 여부 결정 (빈 생성 시점)

// 클래스 레벨에서 한 번이라도 조건 만족하면 프록시 생성
for (메서드 : 클래스의_모든_메서드) {
    if (포인트컷이_메서드에_매칭되면) {
        프록시_생성 = true;
        break;
    }
}

이 단계에서는 클래스의 모든 메서드를 포인트컷에 매칭시켜본다. 하나의 메서드라도 조건에 맞으면 해당 빈은 프록시로 생성된다.

2.4.2. 두 번째 단계: 어드바이스 적용 여부 결정 (메서드 실행 시점)

// 각 메서드 호출 시마다 조건 검증
if (현재_실행_메서드가_포인트컷에_매칭되면) {
    어드바이스_실행();
}
실제_메서드_호출();

이 단계는 프록시가 생성된 후, 실제 메서드가 호출될 때마다 실행된다.

2.5. 예제로 이해하기

다음과 같은 클래스가 있다고 가정하자:

class OrderController {
    void request() {}   // 로깅 대상 (request* 패턴 매칭)
    void noLog() {}     // 로깅 대상 아님
    void orderItem() {} // 로깅 대상 (order* 패턴 매칭)
}

포인트컷: "request*", "order*"

첫 번째 단계 결과:

  • request(): 매칭됨
  • orderItem(): 매칭됨
  • noLog(): 매칭 안됨
  • → 프록시 생성 (하나 이상의 메서드가 매칭되었으므로)

두 번째 단계 (실행 시):

  • request() 호출 → 어드바이스 실행 → 실제 메서드 실행
  • orderItem() 호출 → 어드바이스 실행 → 실제 메서드 실행
  • noLog() 호출 → 어드바이스 실행 안됨 → 실제 메서드 실행

2.6. 왜 두 단계로 나눌까?

이러한 두 단계 구조는 성능과 기능을 모두 고려한 설계이다:

  1. 성능 최적화: 모든 빈에 프록시를 생성하지 않고, 필요할 때만 생성한다
  2. 유연성: 프록시 생성 여부와 어드바이스 적용 여부를 분리하여 제어한다
  3. 정밀도: 메서드 단위로 어드바이스 적용 여부를 결정할 수 있다

2.7. 실제 구현 코드 분석

스프링 내부에서는 다음과 같은 방식으로 동작한다:

// AbstractAutoProxyCreator 클래스의 일부
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 1. 이미 프록시인지 확인
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }

    // 2. Advice 적용 대상인지 확인
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

    // 3. 적용 대상이면 프록시 생성
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    // 4. 적용 대상이 아니면 원본 반환
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

2.8. @Aspect의 진정한 가치

@Aspect는 단순히 코드를 줄여주는 도구가 아니다. 그것은 프로그래밍 패러다임의 변화를 의미한다:

명령형(Imperative) → 선언적(Declarative)

// 이전: 어떻게(How)를 명시
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
proxyFactory.addAdvisor(advisor);

// 이후: 무엇을(What)을 선언
@Around("execution(* com.example..*(..))")
public Object advice(ProceedingJoinPoint joinPoint) {
    // 비즈니스 로직
}

설정(Configuration) → 표현(Expression)

  • 복잡한 설정 코드 대신 간결한 표현식
  • 의도를 명확히 전달
  • 가독성 향상

2.9. 횡단 관심사(Cross-cutting Concerns)의 완전한 해결

지금까지 진행한 로그 추적 기능은 특정 기능 하나에만 국한되지 않는다. 이는 애플리케이션의 여러 기능들 사이에 걸쳐있는 관심사이다. 이것이 바로 횡단 관심사(cross-cutting concerns)이다. @Aspect는 이러한 횡단 관심사를 다음과 같이 해결한다:

  1. 분리(Separation): 핵심 비즈니스 로직과 부가 기능 분리
  2. 모듈화(Modularization): 재사용 가능한 AOP 모듈 생성
  3. 중앙화(Centralization): 한 곳에서 모든 부가 기능 관리

2.10. 발전 과정의 완성

프록시 기술의 진화 과정을 정리하면 다음과 같다:

1. 프록시 패턴 (수동)
   → 2. 동적 프록시 (반자동)
   → 3. 프록시 팩토리 (편리함)
   → 4. 빈 후처리기 (자동화)
   → 5. @Aspect (선언적 프로그래밍)

각 단계마다 개발자의 수고는 줄어들고, 추상화 수준은 높아졌다. @Aspect는 이 발전 과정의 정점에 있다.


3. 정리

3.1. @Aspect의 핵심 가치

  1. 간결성: 복잡한 설정 코드 제거
  2. 가독성: 의도가 명확한 선언적 프로그래밍
  3. 유지보수성: 변경이 필요할 때 한 곳만 수정
  4. 재사용성: 모듈화된 AOP 컴포넌트
  5. 일관성: 애플리케이션 전반에 걸친 일관된 부가 기능 적용

3.2. 실무 적용 시 고려사항

  1. 포인트컷 표현식: 너무 광범위한 표현식은 성능 저하를 초래할 수 있다
  2. 어드바이스 순서: @Order 애노테이션으로 실행 순서를 제어할 수 있다
  3. 예외 처리: @Around 어드바이스에서는 예외를 적절히 처리해야 한다
  4. 프록시 한계: 생성자, static 메서드, private 메서드에는 적용되지 않는다

3.3. 다음 단계: 스프링 AOP의 개념적 이해

 @Aspect를 통해 AOP의 실용적인 측면을 배웠다. 이제 AOP의 개념적 배경과 이론적 기초를 이해할 차례이다. 다음 장에서는 AOP의 기본 개념, 용어, 다양한 적용 방식에 대해 깊이 있게 알아볼 것이다. AOP는 단순한 기술이 아니라 소프트웨어 설계의 새로운 관점이다. @Aspect는 그 관점을 코드로 표현하는 가장 우아한 방법이다.

'Spring > Core' 카테고리의 다른 글

[Advanced-10] 스프링 AOP 구현  (0) 2026.01.02
[Advanced-9] 스프링 AOP 개념  (0) 2026.01.02
[Advanced-7] 빈 후처리기  (0) 2026.01.02
[Advanced-6] 스프링 지원 프록시  (0) 2026.01.02
[Advanced-5] 동적 프록시 기술  (0) 2026.01.02
'Spring/Core' 카테고리의 다른 글
  • [Advanced-10] 스프링 AOP 구현
  • [Advanced-9] 스프링 AOP 개념
  • [Advanced-7] 빈 후처리기
  • [Advanced-6] 스프링 지원 프록시
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Advanced-8] @Aspect AOP
상단으로

티스토리툴바