개발/Spring

webflux + reactive redis cache 적용하기

갓생사는 김초원의 개발 블로그 2021. 8. 23. 02:13

webflux + reactive redis cache

redis는 캐시로 사용할 수 있음. AOP에서 redis에 원하는 데이터가 있으면 바로 get, 없으면 set하는 로직.

기존 mvc 모델에서는 캐시 어노테이션과 AOP로 캐시를 쉽게 구현할 수 있음. 그런데 webflux 모델에서는 캐시를 구현할 수 가 없었다.

 

mvc 모델 

@RedisCacheable(DEAL)
public Deal getDeal(final long dealNo, ...) {
    // 실제 데이터를 리턴
}

 

webflux 모델

webflux는 데이터를 get하면 Mono/Flux 체인이 리턴된다.

Mono/Flux는 Reactive Streams에서 데이터를 제공하는 발행자 역할을 하는 Publisher의 구현체이다. 구독자가 subscribe()하기 전까지는 실데이터를 얻을 수 없다. 

webflux로는 기존 mvc 코드로 AOP 내에서 캐시 구현을 할 수가 없어서 다른 방법이 필요하다. 

@RedisCacheable(DEAL)
public Mono<Deal> getDeal(final long dealNo, ...) {
// Mono 체인을 리턴
}

 

Reactor Addons 라이브러리를 사용하기

Reacotr Addons 라이브러리를 사용하면 기존 mvc 모델에서처럼 annotation + AOP 방식으로 캐시를 구현할 수 있다. 

implementation("io.projectreactor.addons:reactor-extra:3.3.0.RELEASE")

 

Reactor Addons의 CacheMono, CacheFlux는 아래와 같은 구조로 되어있다.

해당 클래스를 AOP에서 적절히 적용하여 webflux에서 reactive redis 캐시를 구현할 수 있다.

 

  • AOP에 적용한 코드 예시(AOP + redis)
    public class ReactiveCacheAspect {
        private final ReactiveRedisTemplate reactiveRedisTemplate;
     
        @Around("@annotation(com.example.reactiverediscache.redis.RedisCacheable)")
        public <T> Mono cache(ProceedingJoinPoint joinPoint) {
            final String cacheKey = (String) joinPoint.getArgs()[0];
     
            return CacheMono
                    .lookup(k -> {
                        Mono<T> cacheValue = reactiveRedisTemplate.opsForValue().get(cacheKey);
                        return cacheValue.map(Signal::next);
                    }, cacheKey)
                    .onCacheMissResume(() -> Mono.defer(() -> {
                        try {
                            return (Mono<T>)joinPoint.proceed();
                        } catch (Throwable throwable) {
                            throwable.printStackTrace();
                        }
                        return null;
                    }))
                    .andWriteWith((k, signal) -> Mono.fromRunnable(() -> {
                        if (!signal.isOnError()) {
                            reactiveRedisTemplate.opsForValue().set(cacheKey, signal.get()).subscribe();
                        }
                    }));
        }
    }​

정말 성능이 좋아질까(Webflux vs MVC)

테스트 상황) for loop 3번 돌며 redis에 데이터 set 

  • mvc + 일반 redis
  vuser RunTime TPS Peak TPS Executed Test Successful Tests  
mvc
+
일반 redis
30
(Process:2, Threads: 15)
3분 1,196.4 1,464.5 210,758 210,758
50
(Process:2, Threads: 25)
3분 1,416.6 1,846.0 246,730 246,730
70
(Process:2, Threads: 35)
3분 1,554.4 1,971.5 273,891 273,891

 

  • webflux + reactive redis

vuserRun timeTPSPeak TPSExecuted TestsSuccessful Tests

  vuser Run Time TPS Peak TPS Executed Test Successful Tests  
webflux
+
reactive redis
30
(Process:2, Threads: 15)
3분 2,316.3 3,541.0 408,263 408,263
50
(Process:2, Threads: 25)
3분 2,579.1 4,158.5 449,646 449,646
70
(Process:2, Threads: 35)
3분 2,675.1 3,546.5 466,313 466,313

 

but, cache 구현에서는 기존 redis cache와 비교했을 때 webflux + reactive redis의 TPS가 높아지지는 않았음. 비슷하거나 오히려 약간 더 낮았다. 


결론 

 리액티스 프로그래밍을 사용한다고 해서 기본 방식보다 성능이 월등히 높아지는 것은 아니다. 

오히려 발행/구독 형식으로 되어 있기 때문에 단일 작업에서는 성능이 약간 느려질 수 있다. 대신 동시 호출이 많은 작업에 webflux를 사용하면 좋을 것 같다. 

하지만 더 적은 쓰레드와 더 적은 하드웨어 리소스(CPU, 메모리 사용률)로 동시성이 높아진다. 

동시성을 높이는 방식으로 반응을 즉각적으로 하기 때문에 적은 리소스로도 높은 반응을 보장할 수 있다는 것이지 기존 서버의 처리 속도를 올려주는 형태는 아니다. 

하지만 동시성이 높다는 것은 같은 서버로 더 많은 처리를 할 수 있기 때문에 결론적으로 성능이 높아진 것 같은 효과를 누릴 수 있다.