[Spring + Redis] Redis cache를 이용한 조회 성능 개선

가오리·2024년 2월 18일
0

BackEnd

목록 보기
2/13
post-thumbnail

Redis란?

Redis인메모리 데이터 저장소로, 데이터를 메모리에 저장하여 빠른 읽기 및 쓰기 작업을 지원한다. 이는 디스크 기반의 데이터베이스 시스템과는 달리, 데이터를 메모리에 유지함으로써 빠른 응답 시간과 높은 처리량을 제공한다.

캐시를 사용할 때 Redis를 선택하는 이유

  1. 성능 : Redis는 메모리 기반의 데이터 저장소이므로 매우 빠른 읽기 및 쓰기 작업을 지원한다. 따라서 캐시로 사용할 경우, 데이터에 빠르게 접근하여 응답 시간을 단축시킬 수 있다.

  2. 확장성 : Redis는 분산 환경에서 높은 확장성을 제공한다. 여러 개의 Redis 인스턴스를 클러스터링하고 데이터를 분산하여 처리할 수 있으며, 이를 통해 시스템의 성능과 처리량을 향상시킬 수 있다.

  3. 다양한 데이터 구조 : Redis다양한 데이터 구조를 지원하며, 이를 활용하여 캐싱 이외에도 다양한 용도로 활용할 수 있다. 예를 들어, 리스트, 해시, 세트 등의 데이터 구조를 활용하여 다양한 데이터 처리 작업을 수행할 수 있다.

  4. 지속성 : Redis는 메모리 기반의 데이터 저장소이지만, 디스크에 데이터를 지속적으로 저장할 수 있는 기능을 제공한다. 이를 통해 시스템 재시작 시에도 데이터의 유실을 방지할 수 있다.

캐싱 전략 (Caching Strategies)

크게 읽기 전략과 쓰기 전략이 있다.

그 중에 이번 프로젝트는 읽는 작업(설문 조회)이 많기 때문에 읽기 전략 중 Look Aside 전략, 읽을 때 데이터가 없으면 캐시를 저장하는 전략을 사용한다.

캐싱 적용

spring-data-redis 의존성 추가 (build.gradle)

  • implementation 'org.springframework.boot:spring-boot-starter-data-redis

Application 메인 메소드에 @EnableCaching 적용

@EnableCaching
@SpringBootApplication
public class SurveyDocumentApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

@EnableCaching 은 스프링 프레임워크의 캐시 관리 기능을 활성화하는 어노테이션이다. 이를 사용하면 애플리케이션에서 캐시를 사용하겠다는 것을 명시적으로 선언하고, 캐시 관련 설정을 일관되게 관리할 수 있다.

application.propertiesRedis 의 위치 추가

(저는 spring 프로젝트와 같은 container에 redis를 배포하기 때문에 localhost로 지정하였습니다.)

## Redis 설정 추가
spring.cache.type=redis
spring.cache.redis.host=localhost
spring.cache.redis.port=6379

Redis 설정 클래스 생성

  • RedissionConfig

    @Configuration
    public class RedissonConfig {
      @Value("${spring.cache.redis.host}")
      private String redisHost;
      @Value("${spring.cache.redis.port}")
      private int redisPort;
    
      private static final String REDISSON_HOST_PREFIX = "redis://";
    
      @Bean
      public RedissonClient redissonClient() {
          Config config = new Config();
          config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
          return Redisson.create(config);
      }
    
      @Bean
      public RedisConnectionFactory redisConnectionFactory() {
          return new LettuceConnectionFactory(redisHost, redisPort);
      }
    
      @Bean
      public CacheManager cacheManager() {
          RedisCacheManager.RedisCacheManagerBuilder builder =
                  RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
          RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                  .serializeValuesWith(
                          RedisSerializationContext.SerializationPair.fromSerializer(
                                  new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
                  .entryTtl(Duration.ofMinutes(30)); // 캐시 수명 30분
          builder.cacheDefaults(configuration);
          return builder.build();
      }
    }

어노테이션으로 캐시 적용

  • @Cacheable
     // 설문 조회
      @GetMapping(value = "/survey-list/{id}")
      @Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
      public SurveyDetailDto readDetail(@PathVariable Long id) {
          return surveyService.readSurveyDetail(id);
      }
    • 읽기 작업을 수행하는 메서드에 사용되는 어노테이션이다.
    • value : 이 캐시의 제목이다.
    • key : 캐시의 키를 지정한다. "'survey-' + #id" : 여기서는 survey-(단순 String) 와 경로 변수인 id(GetMapping으로 받는 {id})를 조합하여 키를 생성한다. 캐시는 이 키를 기반으로 결과를 저장하고 조회한다.
    • cacheManager : Redis 설정에 정의한 cacheManager를 사용한다.
  • @CacheEvict
     // 설문 수정
      @PutMapping("/update/{id}")
      @CacheEvict(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
      public void updateSurvey(HttpServletRequest request, @RequestBody SurveyRequestDto requestDto, @PathVariable Long id) {
          surveyService.updateSurvey(request, requestDto, id);
      }
    • 삭제 작업을 수행하는 메서드에 사용되는 어노테이션이다.
    • value : survey 라는 이름의 캐시를 대상으로 지정한다. 이 이름은 캐시 관리자에서 식별하는 데 사용된다.
    • key : 삭제할 캐시의 키를 지정한다.'survey-' + #id : survey-와 경로 변수인 id(PutMapping으로 받는 id)를 조합하여 키를 생성한다. 해당 키에 해당하는 캐시가 삭제된다.

작동 과정

  • 먼저 id1 인 설문을 조회한다고 생각해보자. @Cacheable 의 어노테이션이 작동하여 valuesurvey, keysurvey-1로 지정되어 id1 인 메소드의 반환 값인 SurveyDetailDto 가 저장된다. (캐시에 아무것도 저장되어 있지 않다는 전제하에)

  • 그 후, id1 인 설문을 수정한다고 생각해보자. @CacheEvict 의 어노테이션이 작동하여 valuesurvey인 것들을 찾고, 그 중에 keysurvey-1인 캐시 데이터를 확인하여 캐시 목록에서 삭제한다.

  • 만약 캐시가 저장만되고 @CacheEvict는 작동이 안된다면 어떻게 될까?

    • id1 인 설문을 아무리 수정해도 값을 조회하면 캐시에 저장되어 있는 수정되기 전의 설문을 조회할 것이다.
    • 이렇게 캐싱을 적용할 때, 삭제해야 할 캐시를 명확하게 고려하여 어노테이션을 적용해야 데이터의 무결성이 보장됩니다.

캐시 적용 후 성능 테스트

  • 설문 조회시 캐시 저장
  • 설문 수정과 삭제 시 해당하는 설문 id 를 키 값으로 캐시 삭제
@PostMapping(value = "/survey-list")
@Cacheable(value = "surveyPage", key = "'surveyPage-' + #pageRequest", cacheManager = "cacheManager" )
public Page<SurveyPageDto> readList(HttpServletRequest request, @RequestBody PageRequestDto pageRequest) {
    return surveyService.readSurveyList(request, pageRequest);
}

@GetMapping(value = "/survey-list/{id}")
@Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager" )
public SurveyDetailDto readDetail(@PathVariable Long id) {
    return surveyService.readSurveyDetail(id);
}

@GetMapping(value = "/getSurveyDocument/{id}")
@Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
public SurveyDetailDto readDetail1(@PathVariable Long id) {
    return surveyService.readSurveyDetail(id);
}

@GetMapping(value = "/getSurveyDocument2/{id}")
@Cacheable(value = "survey2", key = "'survey2-' + #id", cacheManager = "cacheManager")
public SurveyDetailDto2 readDetail2(@PathVariable Long id) {
    return surveyService.readSurveyDetail2(id);
}
  • 각 캐시를 저장할 때 구분할 수 있는 값을 key에 id 로 추가해주었다.
@Transactional
@Caching(evict = {
        @CacheEvict(value = "surveyPage", allEntries = true, cacheManager = "cacheManager"),
        @CacheEvict(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager"),
        @CacheEvict(value = "survey2", key = "'survey2-' + #id", cacheManager = "cacheManager")
})
public void deleteSurvey(HttpServletRequest request, Long id)@Transactional

@Caching(evict = {
      @CacheEvict(value = "surveyPage", allEntries = true, cacheManager = "cacheManager"),
      @CacheEvict(value = "survey", key = "'survey-' + #surveyId", cacheManager = "cacheManager"),
      @CacheEvict(value = "survey2", key = "'survey2-' + #surveyId", cacheManager = "cacheManager")
})
public void updateSurvey(HttpServletRequest request,SurveyRequestDto requestDto, Long surveyId) {
  • pagination 캐시는 설문이 수정되거나 삭제될 시 전부 삭제 하도록 설정
  • 설문 조회하는 두 가지 방식은 각 설문의 key 값에 따라 삭제할 캐시를 지정

결과

61ms 에서 9ms 로 6.7배 빨라짐

그 외 캐시 적용

user 조회 - (현재 유저 조회, 유저 업데이트, 유저 삭제)

@GetMapping("/me")
@Cacheable(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public User getCurrentUser(HttpServletRequest request) {
    return userService.getCurrentUser(request);
}

@PatchMapping("/updatepage")
@CacheEvict(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public String updateMyPage(HttpServletRequest request, @RequestBody UserUpdateRequest user) {
    return userService.updateMyPage(request, user);
}

@PatchMapping("/deleteuser")
@CacheEvict(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public String deleteUser(HttpServletRequest request) {
    return userService.deleteUser(request);
}

survey answer

// 설문 응답 저장
@PostMapping(value = "/response/create")
@Caching(evict = {
    @CacheEvict(value = "responseList", key = "'responseList-' + #surveyForm.id", cacheManager = "cacheManager" ),
    @CacheEvict(value = "getQuestionAnswerByCheckAnswerId", allEntries = true, cacheManager = "cacheManager" )
})
public void createResponse(@RequestBody SurveyResponseDto surveyForm) 

// 설문 응답들 조회
@GetMapping(value = "/response/{id}")
@Cacheable(value = "responseList", key = "'responseList-' + #id", cacheManager = "cacheManager" )
public List<SurveyAnswer> readResponse(@PathVariable Long id){
    return surveyService.getSurveyAnswersBySurveyDocumentId(id);
}

survey analyze

// 설문 상세 분석 조회
@GetMapping(value = "/research/analyze/{surveydocumentId}")
@Cacheable(value = "surveyAnalyze", key = "'surveyAnalyze-' + #surveydocumentId", cacheManager = "cacheManager" )
public SurveyAnalyzeDto readDetailAnalyze(@PathVariable Long surveydocumentId) {
    return surveyService.readSurveyDetailAnalyze(surveydocumentId);
}

// 설문 분석 시작
@Transactional
@PostMapping(value = "/research/analyze/create")
@CacheEvict(value = "surveyAnalyze", key = "'surveyAnalyze-' + #surveyId", cacheManager = "cacheManager" )
public String saveAnalyze(@RequestBody String surveyId) {
profile
가오리의 개발 이야기

0개의 댓글