본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 요약
오늘 강의에서는 Spring WebFlux 환경에서 Redis를 소개 및 실습을 진행했다. Spring Data Redis Reactive를 통해 비동기 논블로킹 방식으로 Redis와 통신하는 방법을 다루었으며 사용자 조회, 수정 api 기능에 캐싱 기능을 붙이는 내용이었다. 강의에 나온 reactive redis에 대해 더 알아보자.
Reactive Redis의 필요성
Spring WebFlux는 비동기 논블로킹 I/O 기반의 리액티브 프로그래밍 모델을 제공한다. 전통적인 Blocking I/O 방식은 외부 시스템과의 통신 시 스레드가 응답을 기다리며 대기 상태로 전환되는데, 이는 제한된 스레드 풀에서 심각한 병목을 발생시킬 수 있다.
Redis와의 통신 역시 네트워크 I/O 작업이므로, WebFlux 애플리케이션에서 동기식 Redis 클라이언트를 사용하면 리액티브 스택의 장점이 상실된다. 다음과 같은 이유로 Reactive Redis 사용이 권장된다.
- 스레드 차단 방지: 네트워크 I/O 대기 시간 동안 스레드가 블로킹되지 않음
- 리소스 효율성: 적은 수의 스레드로 많은 동시 연결 처리 가능
- 백프레셔 지원: Reactive Streams 스펙을 따라 데이터 흐름 제어 가능
- 일관된 프로그래밍 모델: 전체 스택이 리액티브하게 동작하여 통일된 코드 작성 가능
ReactiveRedisTemplate 구성
Spring Data Redis Reactive의 핵심은 ReactiveRedisTemplate이다. 이 템플릿은 Redis 명령을 Mono 또는 Flux로 래핑하여 리액티브 파이프라인에 자연스럽게 통합할 수 있도록 한다.
기본 연결 설정
로컬 환경에서 기본 설정(localhost:6379)을 사용한다면 별도 구성 없이 사용 가능하다. 원격 서버나 다른 포트를 사용하는 경우 다음과 같이 설정한다.
@Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
Template 생성 및 직렬화 설정
ReactiveRedisTemplate 구성 시 반드시 직렬화 전략을 명시해야 한다. 복합 객체를 다루는 경우 Jackson2JsonRedisSerializer를 사용하는 것이 일반적이다.
@Bean
public ReactiveRedisTemplate<String, Employee> reactiveRedisTemplate(
ReactiveRedisConnectionFactory factory) {
StringRedisSerializer keySerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Employee> valueSerializer =
new Jackson2JsonRedisSerializer<>(Employee.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Employee> builder =
RedisSerializationContext.newSerializationContext(keySerializer);
RedisSerializationContext<String, Employee> context =
builder.value(valueSerializer).build();
return new ReactiveRedisTemplate<>(factory, context);
}
직렬화 전략 선택 기준
| Serializer | 대상 타입 | 특징 | 적합한 사용 사례 |
| StringRedisSerializer | String | UTF-8 인코딩, 사람이 읽기 쉬움 | 키, 단순 문자열 값 |
| GenericToStringSerializer | 원시 타입 | toString/valueOf 변환 기반 | 숫자 타입, 열거형 |
| Jackson2JsonRedisSerializer | 복합 객체 | JSON 직렬화, 타입 정보 포함 가능 | DTO, 도메인 객체 |
| JdkSerializationRedisSerializer | Serializable 객체 | Java 직렬화, 버전 관리 복잡 | 레거시 호환성 필요 시 |
- 키는 StringRedisSerializer를, 값은 데이터 타입에 따라 Jackson2JsonRedisSerializer 또는 GenericToStringSerializer를 조합하여 사용하는 것이 일반적이다.
Operations API vs Commands API
Spring Data Redis Reactive는 두 가지 수준의 API를 제공한다.
Operations API (고수준 추상화)
- ReactiveValueOperations, ReactiveListOperations 등의 인터페이스를 통해 Redis 데이터 구조를 객체 지향적으로 다룰 수 있다.
@Autowired
private ReactiveRedisTemplate<String, Employee> redisTemplate;
private ReactiveValueOperations<String, Employee> reactiveValueOps;
@Before
public void setup() {
reactiveValueOps = redisTemplate.opsForValue();
}
@Test
public void givenEmployee_whenSet_thenSet() {
Mono<Boolean> result = reactiveValueOps.set("123",
new Employee("123", "Bill", "Accounts"));
StepVerifier.create(result)
.expectNext(true)
.verifyComplete();
}
Commands API - 저수준 제어
- ReactiveKeyCommands, ReactiveStringCommands 등을 통해 Redis 명령을 직접 제어할 수 있다.
- ByteBuffer를 사용하여 바이트 스트림을 직접 다루므로 더 세밀한 제어가 가능하다.
@Bean
public ReactiveKeyCommands keyCommands(
ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
return reactiveRedisConnectionFactory.getReactiveConnection().keyCommands();
}
@Test
public void givenFluxOfKeys_whenPerformOperations_thenPerformOperations() {
Flux<SetCommand> keys = Flux.just("key1", "key2", "key3", "key4")
.map(String::getBytes)
.map(ByteBuffer::wrap)
.map(key -> SetCommand.set(key).value(key));
StepVerifier.create(stringCommands.set(keys))
.expectNextCount(4L)
.verifyComplete();
}
API 선택 기준
- Operations API 사용이 적합한 경우
- 일반적인 CRUD 작업
- 타입 안전성이 중요한 경우
- 코드 가독성과 유지보수성 우선
- Redis 명령에 대한 깊은 이해 불필요
- Commands API 사용이 적합한 경우
- 성능 최적화가 필수적인 경우
- 배치 처리나 대량 작업
- Redis 특정 기능에 대한 세밀한 제어 필요
- 바이트 수준 최적화 필요
Reactive Redis 사용 시 주의사항
Reactive 환경에서 Rate Limiting을 구현할 때는 원자성 보장 방법에 주의해야 한다.
리액티브 환경의 트랜잭션 제약
- Reactive API와 Redis Transaction(MULTI/EXEC)은 본질적으로 호환되지 않는다.
- MULTI/EXEC는 명령 큐잉 후 일괄 실행하는 방식이지만, 리액티브 체인에서는 각 명령의 결과를 즉시 구독하고 다음 단계로 전달해야 하기 때문이다.
- 아래와 같은 방식은 같은 연결에서 순차 실행을 보장하지만, 완전한 트랜잭션 격리를 제공하지는 않는다.
// 리액티브 환경에서 제한적인 원자성 보장
private Mono<ServerResponse> incrAndExpireKey(String key, ServerRequest request,
HandlerFunction<ServerResponse> next) {
return redisTemplate.execute(new ReactiveRedisCallback<List<Object>>() {
@Override
public Publisher<List<Object>> doInRedis(ReactiveRedisConnection connection) {
ByteBuffer bbKey = ByteBuffer.wrap(key.getBytes());
return Mono.zip(
connection.numberCommands().incr(bbKey),
connection.keyCommands().expire(bbKey, Duration.ofSeconds(59L))
).then(Mono.empty());
}
}).then(next.handle(request));
}
대안적 접근 방식
- Lua Script를 사용하면 INCR과 EXPIRE를 하나의 원자적 연산으로 묶을 수 있으며, 이는 리액티브 환경에서도 완전한 원자성을 보장한다.
| 방법 | 장점 | 단점 | 적용 시나리오 |
| Lua Script | 서버 측 원자성 보장 | 스크립트 관리 복잡도 증가 | 복잡한 비즈니스 로직 |
| Redis Streams | 이벤트 기반 처리 | 구현 복잡도 높음 | 대규모 분산 처리 |
| 단일 명령 사용 | 단순하고 빠름 | 표현력 제한적 | 단순 카운터, 플래그 |
| 낙관적 락 (WATCH) | 충돌 감지 가능 | 재시도 로직 필요, 리액티브와 호환성 낮음 | 저빈도 업데이트 |
참고 출처
- https://www.baeldung.com/spring-data-redis-reactive
- https://redis.io/learn/develop/java/spring/rate-limiting/fixed-window/reactive



