IT/Redis

[Redis] ReactiveRedisTemplate 를 활용한 데이터 동기화 문제 해결의 건

어린이개발자 2025. 2. 24. 16:02

회사에서 Redis 를 활용한 기능을 개발했는데, 그 기능은 같은 화면을 동시에 조회하는 사용자 수가 몇 명인지 확인하는 것이었다.
쇼핑몰을 이용하다 보면 "X 명이 보고 있습니다." 등의 내용을 확인했던 기억이 있어서, 레퍼런스 자료로 쇼핑몰 플랫폼의 기술블로그를 활용했다. 
그렇게 전체적인 틀을 잡아갔는데 데이터 동기화 측면에서 문제가 발생했다.
화면을 떠나 더 이상 조회하지 않는 사용자가 있는데 Redis 의 데이터에는 아직 남아 있어 실제 상황과 일치하지 않는 문제가 있었다.
그런데 Redis 데이터의 만료 시간을 따로 설정하지 않았기 때문에 이러한 불일치가 발생하면 수정할 수 없는 상황이었다. (필자의 경우엔 데이터의 만료 시간을 설정하면 그것대로 이슈가 생기기 때문에 설정하지 않았다.)
이때 화면을 떠나는 경우 중 뒤로가기 버튼을 클릭할 때는 문제가 없었는데, 페이지 자체를 닫거나 URL 수정 후 새로고침 하는 경우가 문제였다.
그래서 해결책을 고민하다가, 백엔드 환경이 Spring WebFlux 비동기 환경이어서 이에 맞는 설정이 되어야 하지 않을까? 라고 생각이 들어 이에 맞는 설정을 추가로 한 후 테스트를 했더니 문제가 해결되었다. (우연히 때려 맞췄다고 해야 하나 싶다...)
 
코드의 AS-IS 와 TO-BE 를 간략히 기록하면 다음과 같다.
그리고 참고로 Redis 는 기본 값인 Lettuce 를 사용했다.
 
1) AS-IS 
- RedisConfig.java 

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setDefaultSerializer(RedisSerializer.string());
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setEnableTransactionSupport(true);
    return redisTemplate;
}

 
- XXXService.java

@Autowired
private RedisTemplate redisTemplate;

// Redis 에서 데이터 조회
List<Map> users = redisTemplate.opsForList().range(특정 key, 0, -1); // 전체 조회

// Redis 에서 데이터 삭제
redisTemplate.opsForList().remove(특정 키, 1, 데이터 객체); // 특정 키 내의 특정 데이터 삭제

 
2) TO-BE

- RedisConfig.java

@Bean
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
    Jackson2JsonRedisSerializer<Object> Serializer = new Jackson2JsonRedisSerializer<>(Object.class);
    
    RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder = RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
    RedisSerializationConext<String, Object> context = builder.value(serializer).hashValue(serializer).hashKey(serializer).build();
    return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, context);
}

 
- XXXService.java

@Autowired
private ReactiveRedisTemplate reactiveRedisTemplate;

// Redis 에서 데이터 조회
Flux<Map> usersFlux = reactiveRedisTemplate.opsForList().range(특정 key, 0, -1); // 전체 조회
Mono<List<Map>> users = usersFlux.collectList();
users.subscribe(user -> {...로직...});

// Redis 에서 데이터 삭제
reactiveRedisTemplate.opsForList().remove(특정 키, 1, 데이터 객체).subsribe(); // 특정 키 내의 특정 데이터 삭제

 
 
Redis 이외에 클라이언트 환경에서도 이슈가 있었는데, 그 내용은 다음과 같다.
사용자가 페이지를 떠날 때 호출하는 함수로 fetch() 대신 sendBeacon() 을 사용했는데, 해당 함수는 전달하는 Content-Type의 형태가 text/plain 이라 서버 단에서 이에 맞춰 설정을 해야 했다.
서버에서는 WebFlux RouterFunction 을 사용하고 있었고, 이를 고려하여 RequestPredicates.contentType(MediaType.TEXT_PLAIN) 을 RouterFunction 의 체인 메소드로 추가하였다.