본문 바로가기
프레임워크/스프링

Spring @Aspect를 사용하여 어드바이저 생성 기능 사용

by so5663 2022. 12. 24.

스프링 애플리케이션에 프록시를 적용하려면 포인트컷과 어드바이스로 구성되어 있는 어드바이저 ( Advisor )를 만들어서 스프링 빈으로 등록하면 된다. 그러면 나머지는 앞서 배운 자동 프록시 생성기가 모두 자동으로 처리해준다. 자동 프록시 생성기는 스프링 빈으로 등록된 어드바이저들을 찾고, 스프링 빈들에 자동으로 프록시를 적용해준다. (물론 포인트컷이 매칭되는 경우에 프록시를 생성한다.)

 

스프링은 @Aspect 애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.

 

지금까지 어드바이저를 직접 만들었던 부분을 @Aspect 애노테이션을 사용해서 만들어보자.

 

LogTrace.java

public interface LogTrace {
    TraceStatus begin(String message);
    void end(TraceStatus status);
    void exception(TraceStatus status, Exception e);
}

 

ThreadLocalLogTrace.java

@Slf4j
public class ThreadLocalLogTrace implements LogTrace {

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

    private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();

    @Override
    public TraceStatus begin(String message) {
        syncTraceId();
        TraceId traceId = traceIdHolder.get();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);

        return new TraceStatus(traceId, startTimeMs, message);
    }

    @Override
    public void end(TraceStatus status) {
        complete(status, null);
    }

    @Override
    public void exception(TraceStatus status, Exception e) {
        complete(status, e);
    }

    private void complete(TraceStatus status, Exception e) {
        Long stopTimeMs = System.currentTimeMillis();
        long resultTimeMs = stopTimeMs - status.getStartTimeMs();
        TraceId traceId = status.getTraceId();
        if (e == null) {
            log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs);
        } else {
            log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString());
        }

        releaseTraceId();
    }

    private void syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }

    private void releaseTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId.isFirstLevel()) {
            traceIdHolder.remove();//destroy
        } else {
            traceIdHolder.set(traceId.createPreviousId());
        }
    }

    private static String addSpace(String prefix, int level) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            sb.append( (i == level - 1) ? "|" + prefix : "|   ");
        }
        return sb.toString();
    }
}

 

 

LogTraceAspect.java

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()); //join point 시그니처
    
     	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;
        }
     }
}
  • @Aspect : 애노테이션 기반 프록시를 적용할 때 필요하다.
  • @Around("execution(* hello.proxy.app..*(..))")
    • @Around 의 값에 포인트컷 표현식을 넣는다. 표현식은 AspectJ 표현식을 사용한다.
    • @Around 의 메서드는 어드바이스( Advice )가 된다. 
  • ProceedingJoinPoint joinPoint : 어드바이스에서 살펴본 MethodInvocation invocation 과 유사한 기능이다. 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함되어 있다.
  • joinPoint.proceed() : 실제 호출 대상( target )을 호출한다.

 

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);
     }
}

 

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();
     }
}

자동 프록시 생성기는 2가지 일을 한다.

1. @Aspect 를 보고 어드바이저( Advisor )로 변환해서 저장한다.

2. 어드바이저를 기반으로 프록시를 생성한다.

 

1. @Aspect 를 어드바이저로 변환해서 저장하는 과정

1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.

2. 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 애노테이션이 붙은 스프링 빈을 모두 조회한다.

3. 어드바이저 생성: @Aspect 어드바이저 빌더를 통해 @Aspect 애노테이션 정보를 기반으로 어드바이저를 생성한다.

4. @Aspect 기반 어드바이저 저장: 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다.

 

2. 어드바이저를 기반으로 프록시 생성 자동 프록시

1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)

2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.

3-1. Advisor 빈 조회: 스프링 컨테이너에서 Advisor 빈을 모두 조회한다.

3-2. @Aspect Advisor 조회: @Aspect 어드바이저 빌더 내부에 저장된 Advisor 를 모두 조회한다.

4. 프록시 적용 대상 체크: 앞서 3-1, 3-2에서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 메서드 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.

5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다. 그래서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

 

정리

@Aspect 를 사용해서 애노테이션 기반 프록시를 매우 편리하게 적용해보았다.

실무에서 프록시를 적용할 때는 대부분이 이 방식을 사용한다.

 

출처 김영한의 스프링 고급편