Redis에서 다수 키의 값을 효율적으로 가져오는 방법

다중 키 조회: multiGet 사용

기본적인 복수 키 조회 기능을 제공하며, 간단한 구현과 사용이 가능하다. 특히 10~200개 내외의 키를 동시에 조회할 때는 성능 저하가 거의 발생하지 않는다.

하지만 키 수가 증가하면 다음과 같은 문제점이 발생할 수 있다:

  • 네트워크 지연: 요청 패킷 크기가 커져 전송 시간이 늘어남
  • 응답 부하: 값이 큰 경우 응답 데이터가 방대해져 클라이언트/서버 메모리 사용량 증가
  • Redis 서버 부하: 동시 요청이 많아지면 처리 지연이나 리소스 과부하 유발
  • 타임아웃 위험: 네트워크 불안정 시 대량 요청으로 인한 실패 가능성

실제 적용에서는 키 수가 수백 개 이내라면 multiGet로 충분히 활용 가능하지만, 천 개 이상의 키를 조회해야 한다면 아래와 같은 대안을 고려해야 한다:

  • 분할 조회: 키 목록을 여러 그룹으로 나누어 각각 multiGet 호출
  • SCAN 명령 활용: 특정 패턴에 맞는 키들을 반복 조회하는 방식 (예: test-*)
  • 파이프라인 사용: 연속된 명령을 한 번에 전송하여 네트워크 라운드 트립 감소
// 예시: 키 여러 개를 한 번에 조회
@Test
public void testMultiGet() {
    List<String> keyList = Arrays.asList("cache:user:1", "cache:user:2", "cache:user:3");
    int idx = 1;
    for (String key : keyList) {
        stringRedisTemplate.opsForValue().set(key, "user_data_" + idx++, Duration.ofHours(1));
    }

    List<String> result = stringRedisTemplate.opsForValue().multiGet(keyList);
    System.out.println("조회 결과: " + result);
}

파이프라인을 통한 최적화: executePipelined 사용

여러 독립적인 명령을 하나의 네트워크 요청으로 묶어 전송함으로써 성능을 극대화한다. 특히 대량 읽기 또는 쓰기 작업에서 효과적이다.

단, 코드 구조가 복잡해지고, 결과 수집을 수동으로 관리해야 하며, 과도한 사용은 서버 부하를 초래할 수 있다.

@Test
public void testPipelineFetch() {
    List<String> keys = Arrays.asList("data:order:1001", "data:order:1002", "data:order:1003");

    // 초기 데이터 설정
    keys.forEach(k -> stringRedisTemplate.opsForValue().set(k, "order_" + k.split(":")[2], Duration.ofMinutes(5)));

    // 파이프라인으로 병렬 조회
    List<Object> results = stringRedisTemplate.executePipelined(connection -> {
        keys.forEach(k -> connection.get(k.getBytes()));
        return null; // 결과는 외부에서 반환됨
    });

    results.stream()
           .filter(Objects::nonNull)
           .map(b -> new String((byte[]) b))
           .forEach(System.out::println);
}

주의: 파이프라인 내에서는 MULTI, EXEC 등의 트랜잭션 명령을 포함하면 안 된다. 이를 포함하면 예외 발생.

패턴 기반 키 탐색: SCAN 명령 사용

KEYS *처럼 전체 키를 일괄 조회하는 것은 성능 장애를 유발할 수 있으므로, SCAN은 비차단 방식으로 대용량 키 탐색에 적합하다.

단, 결과는 커서 기반으로 반환되며, 한 번에 최대 count만큼의 키만 반환되기 때문에 반복 호출이 필요하다.

count 값은 100 정도로 설정해 성능과 메모리 사용 사이의 균형을 유지하는 것이 좋다.

@Test
public void testScanWithPattern() {
    // 테스트 데이터 생성
    IntStream.rangeClosed(1, 5).forEach(i -> {
        String key = "log:entry:" + i;
        stringRedisTemplate.opsForValue().set(key, "event_data_" + i, Duration.ofDays(1));
    });

    Set<String> matchedKeys = stringRedisTemplate.execute((connection) -> {
        Set<String> result = new HashSet<>();
        Cursor<byte[]> cursor = connection.scan(
            ScanOptions.scanOptions()
                .match("log:entry:*")
                .count(100)
                .build()
        );

        while (cursor.hasNext()) {
            result.add(new String(cursor.next()));
        }
        return result;
    });

    System.out.println("매칭된 키들: " + matchedKeys);
}

이 방법은 KEYS 명령보다 안전하지만, count 값을 너무 크게 설정하면 동일한 성능 문제를 겪을 수 있으니 주의 필요.

결론

각 방법은 상황에 따라 최적의 선택이 다르다. 작은 규모의 키 조회에는 multiGet, 대량의 독립적 접근에는 executePipelined, 패턴 기반 검색에는 SCAN이 적합하다. 실제 환경에서의 성능 테스트를 통해 가장 적절한 전략을 결정하는 것이 중요하다.

태그: Redis Spring Data Redis multiGet Pipeline SCAN

6월 4일 16:23에 게시됨