[Spring + Redis] Cache를 통해 성능 개선하기

Dev_ch·2023년 2월 28일
7
post-thumbnail

🫢 Redis를 활용하여 key-value 값 저장을 해보고 싶다면?
Redis를 이용해 게시글 조회수 구현해보기

저번 포스팅에선 Redis를 활용해 조회수와 같은 데이터를 key-value 값으로 저장 및 관리하는 기능을 구현하면서 인메모리 데이터베이스를 통해 성능을 개선하고 활용하는 방법 또한 알아보았다. 이번에는 Redis가 사용되는 주 목적이자 성능 개선을 위해 사용되는 Caching을 알아보도록 하자 !

1. Cache란?

🥹 한번의 데이터 요청은 하나의 네트워크 호출인데, 이를 최소화 할 수 있다 !

Cache란, 한번 처리한 데이터를 임시로 저장소에 저장하는 것으로, 이 임시 데이터를 동일하거나 유사 요청이 왔을 경우 저장소에서 바로 읽어와서 응답을 하여 성능 및 응답속도 향상을 위한 기술이다.

💡EX) 상대방의 정보를 조회하는 API가 한번 호출되었다면, 해당 데이터를 임시로 저장소에 그대로 저장한다. 그 이후 클라이언트가 해당 정보를 요청하는 API가 다시 호출이 된다면 별도의 연산 없이 임시에 저장된 데이터를 바로 읽어와서 응답하여 성능과 속도가 개선된다 !

1-1. Cache를 사용하기위해 고려해야하는 부분

  1. 별도의 연산 수행이 없이 동일한 응답 값을 전달할 수 있어야함
  2. 어떤 기준으로 Cache를 적용할지 결정해야함
  3. Cache의 유지 기간을 고려해야함
  4. 저장공간에 대한 설계

해당 항목들을 고려하였을때, Cache는 데이터의 수정이 적고, 연산이나 생성, 수정이 일어나는 호출이라면 적용하기 까다롭다는 점을 알 수 있다.

1-2. Cache는 주로 어디에 사용할까?

  • Content에 대한 정보 조회와 같은 상황
  • 즉시 메시지를 주고 받아야 될 때
  • 장바구니의 삭제
  • 그 외 반복되거나 변경이 잘 되지 않는 data들

결론적으로는 자주 호출되면서 업데이트를 많이 하지 않는 데이터들로 구성되어야 한다. 데이터의 수정이 많다면 DB와 Cache에 대해 실시간성을 확보해야 하기에 해당 일관성을 유지해주는 과정이 지속적으로 들어가게 된다.

1-3. ⭐️ Redis Cache 설계 전략

Cache를 사용하기에 앞서 어떠한 전략으로 적용하여 성능을 개선할지 고민해보아야 한다. 필자의 경우 이번 포스팅에서는 Look Aside + Write Around 전략으로 성능 개선을 구현해보려한다. 해당 설계 전략의 경우 다음에 포스팅에서 조금 더 깊게 다뤄보도록 하겠다. 아래는 Redis Cache 전략에 대해 잘 설명되어있는 포스팅이다.

📚 캐시 설계 전략 지침 총정리


2. Spring에 Redis Cache 적용하기

➡️ Redis 세팅하기

저번 포스팅에서 Redis 세팅을 마쳤다는 가정하에, 바로 Cache를 적용해보도록 하겠다.

RedisCacheConfig.java

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public CacheManager contentCacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
                .entryTtl(Duration.ofMinutes(3L)); // 캐시 수명 30분

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
    }
}

Cache에 대한 Config 클래스를 구현해준다. 해당 오브젝트를 통해 TTL(Time To Live), disableCachingNullValues, key&value 직렬화 등 캐싱 정책을 설정할 수 있다 👍

Application.java


@EnableCaching
@SpringBootApplication
..
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

서버를 실행시키는 SpringBoot Starter class에 적용 클래스에서 @EnableCaching 어노테이션을 사용하여 Caching 기능을 활용될 것 임을 명시해주자.

Service.java

 @Transactional
    @Cacheable(value = "Contents", key = "#contentId", cacheManager = "contentCacheManager")
    public ContentDto.detailDto detailContent(UserDetails userDetails, Long contentId) {
        Content content = getContent(contentId);
        .
        .
        .

        return ContentDto.detailDto.response(
                content,
                .
                .
                .
        );
    }

Redis Cache 활성화를 위한 @Annotation을 사용하였는데. 해당 @Cacheable은 DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는데 사용되며 요청한 데이터가 redis에 존재하지 않을 경우 DB에서 조회하며 redis에 존재할경우 해당 저장소에서 바로 읽어 응답한다.

Value(아무렇게 해도 상관없음!)와 key를 설정하여 호출된 응답을 저장해주며 cacheManger 항목을 이용하여 해당 어노테이션에 적용될 config를 각각 설정할 수 있다.

🤔 그래서 성능 차이가 얼마나 나는데요?

필자가 구현한 조회 API는 여러 상태값과 저장되는 코드들이 존재하여 read하는 API 임에도 불구하고 상당히 무거웠다

⬆️ 평균적으로 180ms 정도의 호출 시간이 걸렸는데 대략 0.18~0.2 초라고 생각한다면, 대규모 클라이언트가 요청한 상황에서는 응답 시간이 매우 길어질 것 이다 (...)

그리고 아래 이미지는 Caching을 적용한 이후이다 ⬇️

평균적으로 20~30ms 정도의 속도를 보여주었는데 호출 시간이 약 0.02~0.03초 까지 획기적으로 줄어들었다 🥹


😱 하지만 문제가 발생해

⚠️ 이건 알아야해 ⚠️
Cache miss : 참조하려는 데이터가 캐시에 존재 하지 않을 때
Cache hit : 참조하려는 데이터가 캐시에 존재할 때

해당 구현을 통해 성능에 대해 확실한 개선이 이루어졌지만, 해당 Cache의 쓰기 전략은 Write Around 이였다. 해당 전략으로 설계하면 모든 데이터는 DB에 저장되며, Cache miss가 발생하는 경우에만 DB와 캐시에도 데이터를 저장하게 된다.

또한 읽기 전략은 Look Aside 였는데, 해당 전략으로 설계하면 데이터를 찾을때 캐시에 저장된 데이터가 있는지 우선적으로 확인하여 만일 캐시에 데이터가 없으면 DB에서 조회함.

즉 한번 호출된 응답 값이 Cache에 저장된다면 Cache miss가 발생하지 않아 클라이언트가 해당 호출을 다시 요청할 경우 Cahce에 있는 응답 값을 바로 전송한다.

해당 상황의 문제점은, 요청한 데이터가 수정되었지만 이미 응답값은 Cache에 저장되어있어 Cache hit가 발생하고 수정된 데이터를 DB에서 응답하는 것이 아닌 Cache에서 응답하여 데이터가 불일치된다. 즉, 데이터의 실시간성이 반영되지 않는다는 것 이다.

이 문제를 해결하기 위해 아래와 같이 구현해보자.

Service.java

@Transactional
    @CacheEvict(value = "Contents", key = "#contentId", cacheManager = "contentCacheManager")
    public ContentDto.UpdateDto updateContent(
		Long contentId, ContentDto.UpdateDto request
    ) {
    .
    .
    .
        return ContentDto.UpdateDto.response(
                contentRepository.save(
                       .
                       .
                       .
                )
        );
    }

컨텐츠를 수정하는 API에서 @CacheEvict 어노테이션을 사용해 저장되어있는 Cache를 제거해준다. 이러한 전략을 펼치면 글이 수정될때 응답이 저장되어있는 Cache가 제거되면서 Cache miss가 발생할 수 있게 해주고 데이터의 실시간성을 확보하여 Cache와 DB의 데이터가 일치하게 된다 !

하지만 해당 상황에서는 @CachePut 어노테이션을 사용하여 데이터를 업데이트 해주는 것이 좋다. 로직을 조금 더 풀어 설명하기 위해 @CacheEvict를 사용하였다. 해당 어노테이션은 데이터가 제거될때 주로 사용해주자.


마지막으로

Redis의 Cache를 활용하는 것은 서버에 성능 개선과 속도를 높일 수 있지만, 그만큼 데이터를 어떻게 처리하는지에 대한 전략을 잘 세우고 사용하는 것이 좋다.

Cahce를 사용한다는 것은 데이터의 실시간성을 어느정도 포기하고, 성능과 속도를 가져간다고 볼 수 있기에 사용하기 전, 전략과 사용방법을 잘 세워보도록 하자 🤔

도움이 된 블로그
https://velog.io/@qotndus43/Cache
https://www.woolog.dev/backend/spring-boot/spring-boot-redis-cache-simple/
https://inpa.tistory.com/entry/REDIS-📚-캐시Cache-설계-전략-지침-총정리#look_aside_패턴
https://inpa.tistory.com/entry/REDIS-📚-개념-소개-사용처-캐시-세션-한눈에-쏙-정리#

profile
내가 몰입하는 과정을 담은 곳

0개의 댓글