본문 바로가기
BackEnd/Project

[PharmNav] Redis vs DB 조회 성능 비교 분석

by 개발 Blog 2024. 9. 24.

데이터베이스 성능 최적화는 현대 애플리케이션 개발에서 핵심적인 요소다. 특히 대규모 데이터를 다루는 서비스에서는 더욱 중요하다. 본 분석에서는 PharmNav 프로젝트의 약국 정보 조회 기능을 중심으로 Redis와 관계형 데이터베이스의 성능을 비교해 본다. 이를 통해 캐시 시스템 도입의 효과와 실제 사용자 경험에 미치는 영향을 살펴보고자 한다.

 

성능 비교를 위해 PharmacySearchService 클래스를 사용한다. 이 서비스는 Redis와 DB에서 약국 정보를 조회하는 기능을 제공한다. 

package com.example.phamnav.pharmacy.service;

import com.example.phamnav.pharmacy.cache.PharmacyRedisTemplateService;
import com.example.phamnav.pharmacy.dto.PharmacyDto;
import com.example.phamnav.pharmacy.entity.Pharmacy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PharmacySearchService {

    private final PharmacyRepositoryService pharmacyRepositoryService;
    private final PharmacyRedisTemplateService pharmacyRedisTemplateService;

    public List<PharmacyDto> searchPharmacyDtoList() {
        // Redis에서 데이터 조회 시간 측정
        long startRedisTime = System.currentTimeMillis();
        List<PharmacyDto> pharmacyDtoList = pharmacyRedisTemplateService.findAll();
        long endRedisTime = System.currentTimeMillis();
        log.info("레디스 조회 시간: " + (endRedisTime - startRedisTime) + " ms");

        if (!pharmacyDtoList.isEmpty()) {
            log.info("Redis findAll success!");
            return pharmacyDtoList;
        }

        // DB에서 데이터 조회 시간 측정
        long startDbTime = System.currentTimeMillis();
        pharmacyDtoList = pharmacyRepositoryService.findAll()
                .stream()
                .map(this::convertToPharmacyDto)
                .collect(Collectors.toList());
        long endDbTime = System.currentTimeMillis();

        // Redis에 데이터 저장
        pharmacyDtoList.forEach(pharmacyRedisTemplateService::save);

        // DB 조회 시간 로그 출력
        log.info("DB 조회 시간: " + (endDbTime - startDbTime) + " ms");

        return pharmacyDtoList;
    }

    private PharmacyDto convertToPharmacyDto(Pharmacy pharmacy) {
        return PharmacyDto.builder()
                .id(pharmacy.getId())
                .pharmacyAddress(pharmacy.getPharmacyAddress())
                .pharmacyName(pharmacy.getPharmacyName())
                .latitude(pharmacy.getLatitude())
                .longitude(pharmacy.getLongitude())
                .build();
    }
}

주요 특징은 다음과 같다.

  • Redis 조회: pharmacyRedisTemplateService.findAll()을 통해 Redis에서 데이터를 먼저 조회한다.
  • DB 조회: Redis에 데이터가 없을 경우, pharmacyRepositoryService.findAll()을 통해 DB에서 데이터를 조회한다.
  • 성능 측정: 각 조회 과정에서 시작 시간과 종료 시간을 기록하여 소요 시간을 측정한다.
  • 캐시 갱신: DB에서 조회한 데이터는 다시 Redis에 저장하여 캐시를 최신 상태로 유지한다.

1. 포스트맨 GET 요청 테스트

Postman은 클라이언트 측에서 서버로 요청을 보내고 받는 전체 시간을 측정한다. 이 시간에는 네트워크 지연, 서버 처리 시간, 데이터 직렬화 및 역직렬화 시간이 포함된다. 실제 사용자가 요청을 보낼 때 경험하는 전체 응답 시간을 반영하므로, 사용자 경험을 평가할 때 유용하다.

 

1) 2만 건

DB(좌) Redis(우)

  • DB 조회 시간은 5.29초, Redis 조회 시간은 215ms이다.

2) 12만 건 

DB(좌) Redis(우)

  • DB 조회 시간은 23.06초, Redis 조회 시간은 748ms이다.

3) 30만 건

DB(좌) Redis(우)

  • Postman의 최대 응답 크기 제한을 초과하여 에러가 발생한다.

2. 애플리케이션 로그를 통한 서버 내 성능 분석

애플리케이션 로그는 서버 측에서 특정 작업(예: DB 조회, Redis 조회 등)을 처리하는 데 걸리는 시간을 측정한다. 네트워크 지연이나 클라이언트 측의 시간은 포함되지 않으므로, 서버 측의 성능을 직접적으로 분석할 때 유용하다.

 

조회 건수별 성능

2만 건

  • DB 조회 시간은 286ms, Redis 조회 시간은 176ms이다.

12만 건

  • DB 조회 시간은 957ms, Redis 조회 시간은 621ms이다.

30만 건

  • DB 조회 시간은 2484ms, Redis 조회 시간은 1514ms이다.
 

테스트 결과, Redis 사용 시 DB 대비 약 28배 빠른 응답 속도를 보였다. 서버 로그 분석에서도 Redis의 우수한 성능이 확인되었으며, 특히 대용량 데이터 처리에서 그 차이가 두드러졌다.

 

이러한 큰 차이의 원인을 파악하기 위해 추가 테스트를 진행했다. 먼저, ping 테스트로 네트워크 지연 시간을 측정했다.

평균 0.148ms의 지연 시간은 무시할 만한 수준으로, Postman과 서버 측 테스트 결과의 큰 차이를 설명하기에는 부족했다.

따라서 더 정확한 분석을 위해 코드를 수정하여 각 처리 단계별 소요 시간을 세밀하게 측정했다.

 

PharmacyRedisTemplateService

레디스 저장 방식을 개별 저장에서 일괄 저장 방식으로 수정하고 로그를 추가한다.

public void saveAll(List<PharmacyDto> pharmacyDtoList) {
    if (pharmacyDtoList == null || pharmacyDtoList.isEmpty()) {
        log.warn("[PharmacyRedisTemplateService saveAll] Empty list provided");
        return;
    }

    try {
        Map<String, String> pharmacyMap = new HashMap<>();
        for (PharmacyDto pharmacyDto : pharmacyDtoList) {
            String jsonString = objectMapper.writeValueAsString(pharmacyDto);
            pharmacyMap.put(pharmacyDto.getId().toString(), jsonString);
        }

        hashOperations.putAll(CACHE_KEY, pharmacyMap);

        log.info("[PharmacyRedisTemplateService saveAll success] {} items saved", pharmacyDtoList.size());

        // 저장 후 데이터 확인
        long savedCount = hashOperations.size(CACHE_KEY);
        log.info("[PharmacyRedisTemplateService saveAll] Total items in Redis: {}", savedCount);
    } catch (JsonProcessingException e) {
        log.error("[PharmacyRedisTemplateService saveAll error] {}", e.getMessage());
    }
}

 

PharmacySearchService

이 클래스도 수정하여 로그를 추가하고, 데이터 조회 및 저장 과정에서 걸리는 시간을 세밀하게 측정한다.

package com.example.phamnav.pharmacy.service;

import com.example.phamnav.pharmacy.cache.PharmacyRedisTemplateService;
import com.example.phamnav.pharmacy.dto.PharmacyDto;
import com.example.phamnav.pharmacy.entity.Pharmacy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PharmacySearchService {

    private final PharmacyRepositoryService pharmacyRepositoryService;
    private final PharmacyRedisTemplateService pharmacyRedisTemplateService;

    public List<PharmacyDto> searchPharmacyDtoList() {
        long startRequestTime = System.currentTimeMillis();

        try {
            // Redis에서 데이터 조회 시간 측정
            long startRedisTime = System.currentTimeMillis();
            List<PharmacyDto> pharmacyDtoList = pharmacyRedisTemplateService.findAll();
            long endRedisTime = System.currentTimeMillis();
            log.info("레디스 조회 시간: {} ms, 조회된 항목 수: {}", endRedisTime - startRedisTime, pharmacyDtoList.size());

            if (!pharmacyDtoList.isEmpty()) {
                log.info("Redis findAll success!");
                return pharmacyDtoList;
            }

            // DB에서 데이터 조회 시간 측정
            long startDbTime = System.currentTimeMillis();
            List<Pharmacy> pharmacyList = pharmacyRepositoryService.findAll();
            long endDbTime = System.currentTimeMillis();
            log.info("DB 조회 시간: {} ms, 조회된 항목 수: {}", endDbTime - startDbTime, pharmacyList.size());

            pharmacyDtoList = pharmacyList.stream()
                    .map(this::convertToPharmacyDto)
                    .collect(Collectors.toList());

            // Redis에 데이터 저장 시간 측정
            long startRedisSaveTime = System.currentTimeMillis();
            pharmacyRedisTemplateService.saveAll(pharmacyDtoList);
            long endRedisSaveTime = System.currentTimeMillis();
            log.info("Redis 저장 시간: {} ms, 저장된 항목 수: {}", endRedisSaveTime - startRedisSaveTime, pharmacyDtoList.size());

            return pharmacyDtoList;
        } catch (Exception e) {
            log.error("데이터 조회 중 오류 발생", e);
            throw e;
        } finally {
            long endRequestTime = System.currentTimeMillis();
            log.info("전체 요청 처리 시간: {} ms", endRequestTime - startRequestTime);
        }
    }

    private PharmacyDto convertToPharmacyDto(Pharmacy pharmacy) {
        return PharmacyDto.builder()
                .id(pharmacy.getId())
                .pharmacyAddress(pharmacy.getPharmacyAddress())
                .pharmacyName(pharmacy.getPharmacyName())
                .latitude(pharmacy.getLatitude())
                .longitude(pharmacy.getLongitude())
                .build();
    }
}

 

DB에서 조회

 

Redis에서 조회

 

세부적으로 로그를 찍어봤는데도 20초나 차이나는 원인은 쉽게 파악되지 않았다. 네트워크 문제도 아니었기에 (localhost ping 테스트 결과 매우 빠른 응답 시간을 보임), 다음과 같은 요인들을 고려해 볼 수 있었다.

  1. 데이터 직렬화/역직렬화 차이
  2. 클라이언트 처리 시간
  3. Redis 연결 관리
  4. 전체 프로세스 vs 부분 측정 차이

이 중에서 가장 주목할 만한 점은 '전체 프로세스 vs 부분 측정 차이'다. 로그에서는 각 단계별로 부분 측정을 하고 있지만, Postman은 전체 요청-응답 시간을 측정한다. 따라서 로그에 포함되지 않은 추가적인 처리 시간들이 Postman 측정 시간에 포함되는 것이다.

 

결론적으로, 서버 내부 처리 시간(로그에 기록된 시간)과 실제 클라이언트가 경험하는 응답 시간(Postman에서 측정된 시간) 사이에는 여러 단계의 추가 처리와 전송 과정이 포함되어 있어 차이가 발생한다. 특히 대량의 데이터를 다룰 때는 이러한 차이가 더욱 두드러질 수 있다.

이번 테스트를 통해 성능 측정의 복잡성과 다각도 접근의 중요성을 깨달았다. 서버 내부 로그만으로는 사용자의 실제 경험을 완전히 파악하기 어렵다는 사실이 분명해졌고, 이는 엔드-투-엔드 테스트의 필요성을 더욱 강조한다.

 

앞으로 성능 최적화 작업 시에는 서버 로그, 실제 클라이언트 응답 시간, 네트워크 전송 시간 등을 종합적으로 분석할 계획이다. 이런 포괄적인 접근법을 통해 더욱 정확하고 사용자 중심적인 성능 평가가 가능해질 것이다. 궁극적으로 이는 사용자 경험을 실질적으로 개선하는 최적화로 이어질 수 있을 것이다.