본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

 

강의 요약

오늘 강의에서는 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) 충돌 감지 가능 재시도 로직 필요, 리액티브와 호환성 낮음 저빈도 업데이트

 

참고 출처

 

시작 시간
종료 시간
학습 인증 - 디지털 필기
수강 인증

+ Recent posts