이론 정리/Spring boot

Springboot에서 외부 Redis에 값을 넣고 빼보자(feat.pipeline, Spring boot 3.0)

철매존 2023. 10. 20. 02:50
728x90

코드는 https://github.com/RyooChan/redis-pipeline/tree/main 여기서 확인 가능

Springboot에서 외부 Redis에 값을 넣고 빼보자(feat.pipeline, Spring boot 3.0)

해당 테스트를 하기 전에 Redis는 localhost가 아닌 외부에 존재한다고 가정한다. (외부 레디스를 사용하면 네트워크 지연 시간이 발생하게 될 것이다.)

알다시피 레디스는 굉장히 속도가 빠르다.
그렇지만 레디스와 서버는 TCP 네트워크 모델을 기반으로 통신한다.
이게 무슨 뜻일까... 하면, redis와 서버간의 통신 과정에서 TCP 3-way handshake를 따르고, 여러 번 통신을 하면 결국 속도에서 손해를 볼 수밖에 없다는 것이다.

한번 이를 테스트해 보자.

세팅

먼저 우리는 Redis를 써줄 것이다.
그리고 이를 위해 간단히 세팅해주자.

  • build.gradle

    implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.7.2'
    // https://mvnrepository.com/artifact/redis.clients/jedis
    implementation group: 'redis.clients', name: 'jedis', version: '4.4.0'
  • application.yml
spring:
    data:
      redis:
        host: "redis 호스트명"
        port: "redis 포트번호"
        password: "redis 비밀번호"
  • RedisConfig
@Configuration
@EnableTransactionManagement
public class RedisConfig {


    @Value("${spring.data.redis.host}")
    private String redisHost;

    @Value("${spring.data.redis.port}")
    private String redisPort;

    @Value("${spring.data.redis.password}")
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisHost);
        redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort));
        redisStandaloneConfiguration.setPassword(redisPassword);
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
        return lettuceConnectionFactory;
    }

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

}

간단하게 설명하자면

  • Redis와의 연결 메서드
  • Redis와의 호환을 위한 메서드
    • key, value의 직렬화(key를 문자열, value를 object로 저장)

정도를 원함이다.
참고로 우리는 많은 데이터를 한꺼번에 넣는 테스트를 할거기 때문에 Spring의 RedisTemplate에서 제공하는 Transaction관리를 할 수 있도록 세팅해 두었다.

데이터 입력 테스트

이제 아무런 이름의 Service를 만들고, 여기서 테스트해본다.

  • RedisService
@Service
@Transactional
public class RedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

}

이런 식으로 RedisService를 일단 만들어 준다.

  • 메서드 추가
    public void saveData() {
        for (int i = 0; i < 500; i++) {
            String key = "key" + i;
            String val = "value" + i;
            redisTemplate.opsForValue().set(key, val);
        }
    }

그리고 그 RedisService내부에 이런 이름의 메서드를 넣어준다.
대충 보면 500번 값을 넣는 것이다.

이제 테스트해보자

  • RedisServiceTest
@SpringBootTest
public class RedisServiceTest {

    @Autowired
    private RedisService redisService;

}
  • 테스트 작성
    @Test
    void 입력_테스트() {
        long start = System.currentTimeMillis();
        redisService.saveData();
        long end = System.currentTimeMillis();
        System.out.println("소요시간 = " + (end - start) + "ms");
    }

RedisServiceTest에 간단하게 이렇게 소요시간을 확인해보자

image

값을 입력하는데 2436ms의 시간이 소요됐다.
음.. 뭔가 오래 걸린 것 같은 느낌이 든다.

데이터 입력 Pipeline(set)

오래 걸린 이유는 상술했듯 500번의 저장을 위해 500번 redis에 통신했기 때문이다.

당연하지만 redis에서는 이런 경우를 해결하기 위해 pipeline을 지원한다.
간단히 말하자면 그냥 뭉탱이로 처리할 수 있게 해주는 것이다.

이거는 그냥 직접 사용해보자

  • 메서드 추가

    public void saveDataByPipeline() {
        stringRedisTemplate.executePipelined(
            (RedisCallback<Object>) connection -> {
                connection.openPipeline();

                for (int i = 0; i < 500; i++) {
                    StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
                    String key = "key" + i;
                    String val = "value" + i;
                    stringRedisConnection.set(key, val);
                }

                connection.closePipeline();
                return null;
            }
        );
    }

RedisService에 이런 메서드를 추가한다.
대충 코드를 살펴보면 pipeline을 열어서 한꺼번에 처리하는 것이라고 느껴질 것이다.

  • 테스트 작성
    @Test
    void 입력_파이프라인_테스트() {
        long start = System.currentTimeMillis();
        redisService.saveDataByPipeline();
        long end = System.currentTimeMillis();
        System.out.println("소요시간 = " + (end - start) + "ms");
    }

이제 RedisServiceTest에 이런 테스트 메서드를 만들어 테스트해보자

image

음... 빠르다.
잠깐, 그러면 검색하는 경우는 어떨까??

데이터 검색

  • 메서드 추가
    public List<String> findData() {
        List<String> results = new ArrayList<>();

        for (int i = 0; i < 500; i++) {
            String key = "key" + i;
            Object value = redisTemplate.opsForValue().get(key);
            results.add((String) value);
        }

        return results;
    }

RedisService에 다음과 같은 메서드를 추가한다.
딱 봐도 검색용이다.

  • 테스트 작성
    @Test
    void 검색_테스트() {
        long start = System.currentTimeMillis();
        List<String> data = redisService.findData();
        long end = System.currentTimeMillis();
        System.out.println("소요시간 = " + (end - start) + "ms");
        System.out.println(data.toString());
    }

RedisServiceTest에서 이를 테스트해보면 (가져오는 자체의 시간 측정)

image

이렇게 나온다...
검색에서 시간이 엄청나게 소요되는것이 확인된다.
이게 생각보다 중요한데, 서비스에서는 캐싱된 데이터를 읽는 경우가 많고 갱신을 따로 하기 위해 데이터를 나누어 둘 수도 있을 것이다.
그러면 그걸 한꺼번에 가져오는 경우 속도가 많이 느려질수도 있는 것이다.

데이터 검색 Pipeline(get)

  • 메서드 추가
    public List<String> findDataByPipeline() {
        List<Object> values = stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
                for (int i = 0; i < 500; i++) {
                    StringRedisConnection stringRedisConnection = (StringRedisConnection) connection;
                    String key = "key" + i;
                    stringRedisConnection.get(key);
                }
                return null;
            }
        );

        return values.stream()
            .map(value -> (String) value)
            .toList();
    }

RedisService에 다음과 같이 pipeline을 통해 검색하는 메서드를 넣어준다.

  • 테스트 작성
    @Test
    void 검색_파이프라인_테스트() {
        long start = System.currentTimeMillis();
        List<String> dataByPipeline = redisService.findDataByPipeline();
        long end = System.currentTimeMillis();
        System.out.println("소요시간 = " + (end - start) + "ms");
        System.out.println(dataByPipeline.toString());
    }

RedisServiceTest에서 이런 메서드를 테스트하면

image

이런 식으로, 속도가 비교도 안되게 빨라진걸 알 수 있다.

결론

Redis는 보통 서버 외부에 존재하고, 이 처리에 있어 고려해야 할 사항이 많이 있다.
값의 갱신이나 검색에 있어 pipeline을 활용하면 좀 잘 처리할 수 있다.
다만 pipeline은 절대 만능키가 아니다.

  • Atomic / 오류 처리 문제
    • 다수의 데이터 처리를 진행하기 때문에 이를 잘 처리해야 한다.
    • 나는 설정을 해주어서 문제가 없었지만, 만약 뭔가 빼먹은 상태에서 중간에 에러가 난다면? 그럼 망하는거임ㅇㅇ
  • 안써도 되면 쓰지 말자
    • 예를 들어 하나의 명령만을 쓰는 경우는 이걸 쓰면 오히려 오버헤드때문에 느려지게 될 것이다.
  • 코드가 복잡하다
    • 보면 대충 알겠지만... 그냥 쓰는게 훨씬 편하다

이거를 고려하고 쓰자.
그래도 이게 캐시를 써보면 진짜 신세계인것을 알 수 있다.