모르지 않다는 것은 아는것과 다르다.

Spring

프락시 팩토리빈과 @AspectJ

채마스 2022. 3. 11. 00:01

프락시 팩토리 빈

  • 스프링의 AOP 모듈은 다양한 프락시 기술을 일관된 방식으로 사용할 수 있도록, 프락시 팩토리 빈이라는 서비스 추상화를 제공하고 있다.
  • 프락시 팩토리빈은 프락시를 생성해서 빈 객체를 등록해 주는 팩토리 빈이다.
  • JDK 동적 프락시와는 달리 프락시 팩토리 빈은 순수하게 프락시를 생성하는 작업만 담당하고, 프락시를 통해 제공할 부가 기능은 별도 객체로 생성해, 스프링 컨테이너에 빈으로 등록해 줄 수 있다.

 

@Primary
@Bean
public ProxyFactoryBean cachingMovieReaderFactory(ApplicationContext applicationContext) {
    MovieReader target = applicationContext.getBean(MovieReader.class);
    CacheManager cacheManager = applicationContext.getBean(CacheManager.class);

    ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
    proxyFactoryBean.setTarget(target);
    proxyFactoryBean.addAdvice(new CachingAdvice(cacheManager));

    return proxyFactoryBean;
}
  • 위와 같이 target 과 advice 를 ProxyFactoryBean 에 주입해서 빈으로 등록한다.
  • ProxyFactoryBean 는 프락시 기술을 추상화해 일관된 방법으로 프락시를 만들 수 있게 도와주는 서비스 추상화이다.
  • 또한 프락시를 만들기위해, JDK 다이내믹 프락시와 CGLIB 프락시를 지원한다.
    • JDK 다이내믹 프락시는 인터페이스 기반으로 프락시를 만든다. 스프링의 기본 프락시 생성 전략이다.
    • CGLIB는 바이트코드 생성 기술을 이용 상속 기반으로 프락시를 만든다.

 

Advice 구현 예제

 

public class CachingAdvice implements MethodInterceptor {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final CacheManager cacheManager;

    public CachingAdvice(CacheManager cacheManager) {
        this.cacheManager = Objects.requireNonNull(cacheManager);
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        // 캐시된 데이터 존재하면, 즉시 반환
        Cache cache = cacheManager.getCache(invocation.getThis().getClass().getName());
        Object cachedValue = cache.get(invocation.getMethod().getName(), Object.class);
        if (Objects.nonNull(cachedValue)) {
            log.info("returns cached data. [" + invocation + "]");
            return cachedValue;
        }

        // 캐시된 데이터 없으면, 대상 객체에 명령 위임, 반환된 캐시에 저장 후 반환
        cachedValue = invocation.proceed();
        cache.put(invocation.getMethod().getName(), cachedValue);

        log.info("caching return value. [" + invocation + "]");

        return cachedValue;
    }

}
  • MethodInterceptor 는 가장 기본적인 어드바이스로 대상 객체의 메소드가 호출되는 모든 과정을 참여할 수 있는 어드바이스다.
  • 위와 같이 MethodInterceptor 의 invoke 메소드를 구현한다.
  • 파라미터로 MethodInvocation 객체를 받을 수 있는데, 대상 객체와 메소드의 정보를 취득하거나, 실행 후 결과를 받을 수 있다.
  • 먼저 캐시된 데이터가 있다면, 해당 데이터를 반환한다.
  • 하지만 캐시된 데이터가 없다면, 대상 객체에 proceed() 메소드로 명령을 위임하고, 그 결과 값을 캐시에 저장해서 반환한다.
  • 부가 기능인 CachingAdvice 는 한번만 만들어 필요한 곳에 얼마든지 적용할 수 있다.
  • 하지만 부가기능을 적용하기 위해 프락시를 생성해야된다. 만약 부가기능을 적용해야 되는 객체가 늘어나면 스프링 컨테이너에 프락시를 빈으로 등록하는 코드가 중복되어 발생할 것이다.
  • 또다른 문제점은 어드바이스가 대상 객체나 메소드를 가리지 않고, 부가기능을 적용한다는 점이다.
  • 위의 2가지 문제점에 대해서 스프링은 해결법을 제시한다.

 

@Bean
public DefaultAdvisorAutoCrerator defaultAdvisorAutoProxyCreator() {
    return new DefaultAdvisorAutoProxyCreator();
}

@Bean
public Advisor cachingAdvisor(CacheManager cacheManager) {
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedName("load*");

    Advice advice = new CachingAdvice(cacheManager);

    // Advisor = PointCut(대상 선정 알고리즘) + Advice(부가기능)
    return new DefaultPointcutAdvisor(pointcut, advice);
}
  • DefaultAdvisorAutoCrerator 는 일반적으로 사용되는 자동 프락시 생성기로 스프링 컨테이너에 등록된 어드바이저 내의 포인트컷을 통해 프락시 적용 대 상을 확인하고, 적용 대상이면 프락시 생성 후 어드바이저를 연결한다.
  • 위 코드에서 DefaultAdvisorAutoCrerator 는 advice 에 부가기능을 담아서, 자동으로 프락시를 생성해서 스프링 컨테이너에 등록해 준다.
  • 이렇게 되면 프락시를 일일이 설정하는 문제가 해결된다.
  • 또한 pointcut 을 지정해서 대상을 선정할 수 있다. -> 현재는 loadMovies 메소드에 적용하고 싶기 때문에 load 로시작하는 메소드에 모두 pointcut 을 지정해 두었다.
  • 이렇게 되면 어드바이스가 대상 객체나 메소드를 가리지 않고 부가기능을 적용한다는 문제가 해결된다.
  • 또한 위에서는 pointcut.setMappedName("load*") 을 통해서 메소드의 이름으로 pointcut 을 설정했지만 아래와 같이 애노테이션 기반으로도 pointcut 을 지정할 수 있다.
implementation 'javax.cache:cache-api:1.1.1'
  • 위와 같이 의존성을 추가해 주고 아래와 같이 loadMovies 메소드에 @CacheResult 애노테이션을 붙여준다.
public interface MovieReader {

    @CacheResult(cacheName = "movies")
    List<Movie> loadMovies();

}
@Bean
public Advisor cachingAdvisor(CacheManager cacheManager) {
    AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, CacheResult.class);
    Advice advice = new CachingAdvice(cacheManager);

    // Advisor = PointCut(대상 선정 알고리즘) + Advice(부가기능)
    return new DefaultPointcutAdvisor(pointcut, advice);
}
  • AnnotationMatchingPointcut 은 대상 객체나 메소드에 특정 애노테이션이 선언되어 있으면 조인 포인트로 선정하는 포인트컷이다.

 

@AspectJ

  • AspectJ 란 PARC 에서 개발한 자바 프로그래밍 언어용 관점지향 프로그래밍(AOP) 확장 기능이다.
  • 이클립스 재단 오픈 소스 프로젝트에서 독립형 또는 이클립스로 통합하여 이용 가능하다.
  • AspectJ 는 최종 사용자를 위한 담순함과 이용성을 강조함으로써 폭넓게 사용되고 있다. -> 사실상 AOP 에 대해서 표준이 되었다고 생각해도 무방하다.
  • 이전까지 적용했던 AOP 를 보다 간단하고 명료하게 적용하는 방법이 있다. -> 바로 @AspectJ 를 사용하는 것이다.
  • @AspectJ의 문법과 애스펙트 정의 방법을 활용해 스프링 AOP를 적용할 수 있다.
  • 스프링 AOP는 AspectJ에서 사용되는 표현식 언어를 차용해 일부 문법을 확장해서 사용한다.
  • 정규식처럼 간단한 문자열로 복잡한 포인트컷 선정조건을 쉽게 만들어낼 수 있는 강력한 표현식 언어이다.
  • execution, within, @target, @annotation 등과 같은 포인트컷 지정자로 포인트컷 표현식을 작성한다.
  • @Pointcut 애노테이션 또는 @Before, @After, @Around 등 어드바이스 애노테이션에 적용한다.
implementation 'org.aspectj.aspectjweaver:1.9.6'
  • 위와 같이 의존성을 추가해 준다.
@Aspect
public class CachingAspect {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final CacheManager cacheManager;

    public CachingAspect(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Pointcut("@target(moviebuddy.domain.MovieReader)")
    private void performance(){}

    @Around("performance()")
    public Object doCachingReturnValue(ProceedingJoinPoint pjp) throws Throwable {

        // 캐시된 데이터 존재하면, 즉시 반환
        Cache cache = cacheManager.getCache(pjp.getThis().getClass().getName());
        Object cachedValue = cache.get(pjp.getSignature().getName(), Object.class);
        if (Objects.nonNull(cachedValue)) {
            log.info("returns cached data. [" + pjp + "]");
            return cachedValue;
        }

        // 캐시된 데이터 없으면, 대상 객체에 명령 위임, 반환된 캐시에 저장 후 반환
        cachedValue = pjp.proceed();
        cache.put(pjp.getSignature().getName(), cachedValue);
        log.info("caching return value. [" + pjp + "]");

        return cachedValue;
    }

}
  • @Pointcut 애노테이션을 통해서 포인트컷을 설정한다.
  • @Around 애노테이션을 통해서 doCachingReturnValue 가 advice 임을 명시한다.
@Bean
public CachingAspect cachingAspect(CacheManager cacheManager) {
    return new CachingAspect(cacheManager);
}
  • 위와 같이 CacheManager 를 파라미터로 넘겨주면서 CachingAspect 을 빈으로 등록한다.




REFERENCES

'Spring' 카테고리의 다른 글

설정파일(Properties) 관리  (0) 2022.03.19
Null Safety  (0) 2022.03.14
프로파일과 Resource 인터페이스  (0) 2022.03.10
캐시를 사용한 읽기 속도 최적화  (0) 2022.03.10
자바코드로 의존관게 주입하기  (0) 2022.03.10