Cache 적용하기

초보개발·2022년 12월 19일
0

Spring

목록 보기
37/37

캐시를 적용하게 된 계기

현재 만들고 있는 프로젝트에서 내 정보 보기(GET) 기능은 정보가 단순하며 name을 제외한 나머지 항목들은 반복적으로 동일하게 제공되어진다. 또한, 항상 최신 데이터로 유지되지 않아도 핵심 비즈니스 로직에 영향을 주지 않는다고 판단되었고 해당 기능은 user 마이크로서비스에서 사용자의 정보를 가져와야하기 때문에, 조금이라도 blocking되는 상황을 막고자 적용을 고려하게 되었다.

local? global?

캐시를 구현할 때 방법으로 로컬과 글로벌 방식이 있다.

  • local cache: 서버가 자체적으로 캐시 데이터 저장소를 가진다.
    • 데이터를 가져올 때 오버헤드가 적다. (네트워크를 통하지 않기 때문에 속도가 그만큼 빠르다)
    • 서비스의 인스턴스가 여러개 띄워져 있을 때 서버간의 캐시 데이터 일관성이 깨질 수 있다. (일관성 유지를 위해 동기화 비용이 추가적으로 소모된다.)
  • global cache: 따로 외부 서버에 캐시 데이터 저장소를 둔다.
    • 네트워크 통신이 필요하기 때문에 네트워크 I/O 비용이 소모된다.
    • 모든 인스턴스에서 단일 캐시 데이터 저장소를 사용하므로 일관성 유지가 가능하다.

사용자의 정보 보기 기능은 데이터의 일관성이 조금 어긋나더라도 서비스 운영에 큰 타격을 주지 않기 때문에 local cache로 구현하게 되었다.

어노테이션을 이용한 간단 캐시 적용

  1. build.gradle 의존성 추가하기
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  1. application.yml
spring:
	redis:
    	host: localhost
        port: 6379
  1. Redis configuration
@EnableCaching
@Configuration
public class RedisConfig {
...
}
  1. Repository 인터페이스 추가
@Repository
public interface InfoRepository extends CrudRepository<Info, String> {
}
  1. Service에서 @Cacheable 적용하기
    메소드의 리턴값을 기준으로 캐시에 값을 저장한다. 캐시에 데이터가 있다면 캐시 저장소에 존재하는 데이터를 반환하고 없다면 로직 실행후 캐시 저장소에 저장하여 후에 활용될 수 있도록 한다.
    key: 사용자의 userId
	@Cacheable(key = "#id", cacheNames = "user")
    @Override
    public UserDto getUserInfo(String token, Long id) {
        ResponseEntity<UserDto> userInfo = userServiceClient.getUserInfo(token);
        return userInfo.getBody();
    }
  1. @CacheEvict로 캐시 삭제 적용 -> 데이터의 변경이 일어나는 메서드에 추가
    메서드가 호출될 때 저장된 캐시를 삭제한다. 여기서는 닉네임 변경이 발생할 때 캐시에서 삭제되도록 했다.
    @CacheEvict(key = "#id", cacheNames = "user")
    @Override
    public UserInfoDto updateUserName(String token, UpdateNameDto updateNameDto, Long id) {
        ResponseEntity<String> res = userServiceClient.changeName(token, updateNameDto);
        return UserInfoDto.builder()
                .success(true)
                .message(res.getBody())
                .build();
    }

Redis에서는 Byte code로 데이터를 저장하므로 Serializable을 implements 해야한다.

public class Entity implements Serializable {}


redis cli로 확인해본 결과, 사용자 4번의 정보가 레디스에 저장되었다가 닉네임 수정후 정상적으로 삭제되었다.

🖍 결과 비교

  • 캐시 적용 전의 응답 시간: 287ms
  • 캐시 적용 후의 응답 시간: 평균 20ms 가량

하지만...

사용자의 정보는 이름 변경시 삭제되도록 했기 때문에 데이터의 불일치 문제는 해결했지만 사용자가 탈퇴했을 때에는 계속 캐시 저장소에 남아있게 되는 문제점이 있다. 따라서 캐시 데이터에 TTL을 두어 시간이 지나면 삭제되도록 추가해보았다.

CacheManager로 캐시 TTL 관리 추가

  1. CashingConfigurererSupport 클래스를 상속받아 cacheManager() 재정의
  • Serializer
  • Prefix
  • 캐시의 TTL(여기선 1시간으로 설정함)
@Slf4j
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

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

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    @Override
    public CacheManager cacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .prefixKeysWith("user:")
                .entryTtl(Duration.ofHours(1L));

        builder.cacheDefaults(config);
        return builder.build();
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }
}
  1. @Cacheable, @CacheEvict 어노테이션에 cacheManger 추가하기
    @Cacheable(key = "#id", cacheNames = "user", cacheManager = "cacheManager")
    @CacheEvict(key = "#id", cacheNames = "user", cacheManager = "cacheManager")
  1. ttl 설정 확인

    1시간(=3600초) 설정이 반영되었다는 것을 알 수 있다.

느낀점

운영체제에서 LRU 캐시, 캐시의 특징 이론적으로만 배웠었는데 실제로 적용해보고 캐시의 장점을 느낄 수 있어서 좋았다. 여러 기능에 캐시를 남발하는 것보다 어떠한 상황에서 캐시를 적용하고 내부적으론 어떻게 구현되어야 베스트일지 더 연구해 봐야겠지만 말이다.

0개의 댓글