프락시 팩토리 빈
- 스프링의 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
- 박용권님의 스프링러너 스프링 아카데미
- https://nhj12311.tistory.com/470
'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 |