Infra/Redis

[Redis] Redis를 Cache로 사용하기

simDev1234 2022. 11. 11. 16:55

|  Cache란?

용어 What? why? How?
Cache 임시로 데이터를 저장하는 공간 성능 향상 - Look aside Cache, Write Back
- Memcahced, Redis API

 

1. Cache의 배경과 목적

- 파레토의 법칙에 따르면, 80%의 결과는 20%의 원인에 의해 발생한다.

- 다시말해, 사람들이 자주 쓰는 데이터는 정해져 있고 이를 캐싱해서 저장하면 DB에 접근할 필요가 없어진다.

- 서비스를 런칭하고 사용자가 늘어나면 그만큼 DB 작업량도 늘어나기 마련이라 한다.

- 이럴 때 캐시를 사용한다면, 성능을 개선할 수 있다.

 

2.  Cache의 사용 방식

- 캐시는 조회 또는 쓰기를 할 때에 사용될 수 있다.

Look Aside Cache (Lazy Loading) 캐시를 한 번 접근하여 데이터의 존재 유무에 따라 바로 반환 또는 DB (or API) 호출
Write Back 쓰기(Insert) 작업이 많을 때 쿼리를 모아서 배치(Batch)처리

(1) Look aside Cache

- 한 번 조회한 데이터를 캐시에 저장 후, 동일한 데이터를 조회할 때,

  DB에 direct로 접근하지 않고, Look aside(주의를 돌려) 캐시의 내용을 확인한다.

- Process

  - 사용자가 웹서버에 요청을 보낸다.

  - 웹서버는 Cache를 먼저 조회한다.

  - Cache에 데이터가 있다면, 사용자에게 해당 데이터를 반환(Cache Hit)하고,

    Cache에 데이터가 없다면, DB에서 데이터를 조회한 후 해당 데이터를 반환(Cache Miss)한다.

(2) Write Back

- 캐시 안에 일시적으로 저장한 후 한 번에 모아 배치 처리를 하는 것을 말한다.

- 하지만 Write Back을 사용하면 캐시 안의 데이터가 중간에 유실될 수 있기 때문에 사용에 주의가 필요하다.

(3) 그 외에도 Write Through 등등이 있다.

 

|  Redis

REDIS : Remote (외부에 있는) Dictionary (Key-Value형태) Server (서버)

 

1. Redis란?

  한줄 요약 특징
Redis Key-value 형태의 값을 저장할 수 있는 In-Memory 저장소 - Key-Value 형식의 NoSql
- In-memory
- 다양한 데이터 타입 제공
- 주로 캐시로 사용
- 싱글 쓰레드
- Atomic

- Redis는 인메모리 기반의 data structure store로, db/cache/message broker, streaming engine으로 사용될 수 있다.
  * 많은 개발자들은 Redis를 Store가 아닌 Cache라고 분류한다.
  * Redis는 지속성을 보장하기 위해 데이터를 In-Memory 곧, Disk에 저장할 수 있다.

  * 단, In-memory지만 영구적으로 저장할 수 있기도 하다.

- Redis는 NoSQL로 분류되며, Key-Value 형태로 데이터를 저장한다.
- Redis는 문자열, 해쉬, 리스트, Set, sorted Set 형태의 다양한 자료구조를 지원한다.
- Redis는 기본적으로 Single-Threaded하며, 자료구조는 Atomic 한 성질(=원자성, All or nothing)을 가지고 있어

  race condition이 일어나는 것일 예방할 수 있다는 장점을 가지고 있다.

 

2. Redis의 다양한 모드

종류 서버 특징
Single 단일서버 HA미지원
Sentinel 여러서버 Master-slave* 방식, HA지원*
Cluster

* HA(High Availability) : 서버 하나가 죽더라도 나머지 서버들이 서비스를 계속 운영할 수 있도록 하는 전략

* Master-Slave

  - Master : 한 서버에는 하나의 Master노드가 있다.

  -Slave    : Master노드에 대한 복사본

- Cluster 모드에서는, 여러 서버가 있다고 전제할 때, 각 서버의 Master의 복제본은 다른 서버의 Slave에 저장한다.

  이렇게 할 경우, 서버가 다 죽고 하나만 남는다고 하더라도 운영에 지장이 생기지 않게된다.

  1번서버 2번서버 3번서버
Master Master1 Master2 Master3
Slave Master2 복제본 Master1 복제본 Master1 복제본
Slave Master3 복제본 Master3 복제본 Master2 복제본

 

|  Redis 설치하기

** 참고사항 : embeded-redis를 사용할 경우, 아래와 같이 별도로 redis를 설치하지 않고도,
                     스프링 build.gradle에 라이브러리를 추가하는 것만으로도 redis를 사용할 수 있다.

 

1. OS에 맞게 Redis를 설치한다.

https://redis.io/docs/getting-started/

- 윈도우의 경우 리눅스 환경을 만든 후에야 Redis를 설치할 수 있다.

- 윈도우에서 설치할 수 있는 다른 방법이 있는데 아래 사이트를 가면 된다.

https://github.com/microsoftarchive/redis/releases

* 자세한 설치과정에 대한 포스팅

 

[REDIS] 📚 Window10 환경에 Redis 설치 & 설정

Redis 윈도우 설치 Redis 다운로드 페이지로 이동하여 설치 프로그램을 다운로드하고 설치를 진행한다. Releases · microsoftarchive/redis Redis is an in-memory database that persists on disk. The data model is key-value, but

inpa.tistory.com

 

2. redis-cli.exe를 실행한 후, Redis 명령어를 입력해본다.

* Redis gate에 가면 더 자세한 명령어를 확인할 수 있다.

  http://redisgate.kr/redis/introduction/redis_intro.php

• $> set myKey myValue
• $> OK


• $> get myKey
• $> myValue


• $> del myKey
• $> myValue


• $> get myKey

• $> (nil) 

• $> keys*             <-- 모든 데이터 확인 (실제 운영 시에는 사용X)

• $> set key value <-- redis-server.exe 서버 종료

 

|  스프링에서 Redis 사용하기

 

1. 시작하기

(1) Build.gradle에 Redis 라이브러리 추가

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

(2) application.yml에 redis ip주소 및 port번호 입력

redis:
  host: localhost
  port: 6379

(3) @SpringBootApplication 클래스에 @EnableCaching을 추가

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class BaedanguemApplication {

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

    }

}

 

2. RedisConnectionFactory 및 CacheManager Bean 생성

(1) RedisConnectionFactory 빈 생성

- 사용하는 모드에 따라, Configuration인스턴스를 생성한 후, host, port, password 등을 setting한다.

- 이후 LettuceConnectionFactory에 해당 configuration을 넣어 반환하면 빈 생성이 완료된다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){

        // Single 모드
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();

        // Cluster 모드
        // RedisClusterConfiguration conf = new RedisClusterConfiguration();

        conf.setHostName(host);
        conf.setPort(port);

        return new LettuceConnectionFactory(conf);
    }

}

* C:\Program Files\Redis 의 redis.windows-service.conf 파일에서 port, required password 등을 설정할 수 있다.

 

(2) CacheManager 빈 생성

- RedisConnectionFactory를 매개변수로 담은 메소드를 생성한다. (반환 값은 CacheManager)

- RedisCacheConfiguration.defaultCacheConfig()key, value에 대한 Serialization 방식을 설정한다.

!! Redis에 data 또는 Object를 저장하기 위해 직렬화(Serialization)를 통해 data, object를 Byte 단위로 변경한다.

  * Redis는 Java 시스템 외부의 캐시 서버이기 때문에, 외부에 데이터를 저장하기 위해서는 직렬화가 필요하다.

  * 마찬가지로 Redis에서 데이터를 가져와 data, object로 변환할 때에 역직렬화를 사용한다.

- RedisCacheManager.RedisCacheManagerBuilder를 통해

  매개변수의 RedisConnectionFactory와, 바로 위에서 작성한 RedisCacheConfiguration을 담아 반환한다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

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

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

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){

        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(conf)
                .build();
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){

        // Single 모드
        RedisStandaloneConfiguration conf = new RedisStandaloneConfiguration();

        // Cluster 모드
        // RedisClusterConfiguration conf = new RedisClusterConfiguration();

        conf.setHostName(host);
        conf.setPort(port);

        return new LettuceConnectionFactory(conf);
    }

}

 

3. 조회 요청 시 캐시 저장하기

(1) 조회 요청시 호출되는 메소드에 @Cacheable(key = "", value = "") 추가하기

@Service
class XXXService{

	// 사전 고려사항 : 
	// (1) 자주 요청되는 데이터인가? 그렇다면 캐싱
	// (2) 변경이 잦은 데이터인가?   그렇다면 캐싱x

	// redis server의 key-value와 다른 개념
	@Cacheable(key = "#companyName", value = "finance")
	public ScrapedResult getDividendByCompanyName(String companyName) {
		// 메소드 내용
	}
    
}

ㄴ value 값을 일일히 입력하지 않고, 상수화하여 사용할 수도 있다.

package com.example.baedanguem.model.constants;

public class CacheKey {
    
    public static final String KEY_FINANCE = "finance";
    
}

 

(2) Http 요청을 보낸 후 결과 확인하기

- 수업에서는 Serialization 관련하여 InvalidDefinitionException 오류가 발생했다.

- 원인 : LocalDateTime 타입의 데이터를 캐시에 저장하기 위해 직렬화 하는 과정에서 타입 미스매치 발생

   - 3.에서 직/역직렬화를 StringRedisSerializer가 담당하고 있었기 때문에 LocalDateTime은 직렬화할 수 없다.

- 해결 방법 : 아래 확인

 

(3) 웹서버<->캐시 data 직렬화, 역직렬화  과정에서 타입 미스매치 시

- 수업에서는 Json 타입의 데이터를 송수신 하기 때문에 @JsonSerialize와 @JsonDeserialize를 통해

  직렬화/역직렬화 방식을 지정해주었다.

@Data
@Builder
public class Dividend {

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime date;

    private String dividend;

}

>> 이 상태에서, 프로그램을 실행한 후, 컨트롤러를 통해 최초로 데이터를 조회할 경우, 정상적으로 결과값을 받을 수 있다.

>> 그리고 redis-cli.exe에서 keys * 를 입력해 캐싱이 잘 되었는지 확인하면 결과는 아래처럼 나타난다.

127.0.0.1:6379> keys *
1) "finance::3M Company"
127.0.0.1:6379> get "finance::3M Company"
"{\"@class\":\"com.example.baedanguem.model.ScrapedResult\",\"company\":{\"@class\":\"com.example.baedanguem.model.Company\",\"ticker\":\"MMM\",\"name\":\"3M Company\"},\"dividends\":[\"java.util.ArrayList\",[{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2016,11,16,0,0],\"dividend\":\"1.11\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,2,15,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,5,17,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,8,23,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2017,11,22,0,0],\"dividend\":\"1.175\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,2,15,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,5,17,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,8,23,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2018,11,21,0,0],\"dividend\":\"1.36\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,2,14,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,5,23,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,8,15,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2019,11,21,0,0],\"dividend\":\"1.44\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,2,13,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,5,21,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,8,21,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2020,11,19,0,0],\"dividend\":\"1.47\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,2,11,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,5,20,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,8,20,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2021,11,18,0,0],\"dividend\":\"1.48\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,2,17,0,0],\"dividend\":\"1.49\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,5,19,0,0],\"dividend\":\"1.49\"},{\"@class\":\"com.example.baedanguem.model.Dividend\",\"date\":[2022,8,19,0,0],\"dividend\":\"1.49\"}]]}"

>> 그런데, 이 상태에서 다시 컨트롤러를 통해 동일한 조회 요청을 보내면

     이번엔 아래와 같이 Deserialization을 하려는 Object가 생성자가 없는 Object라는 오류가 나타난다.

     - 원인 : Object에 생성자가 없기 때문에

     - 의문 :  왜 처음에는 오류가 안 났나? == 처음에는 캐시에서 내용을 찾아오지 않고, DB에서 찾아와서.

     - 해결 : Object에 @NoArgsConstructor나 @AllArgsConstructor를 추가한다.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.baedanguem.model.Company` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"com.example.baedanguem.model.ScrapedResult","company":{"@class":"com.example.baedanguem.model.Company","ticker":"MMM","name":"3M Company"},"dividends":["java.util.ArrayList",[{"@class":"com.example.baedanguem.model.Dividend","date":[2016,11,16,0,0],"dividend":"1.11"},{"@class":"com.example.baedanguem.model.Dividend","date":[2017,2,15,0,0],"dividend":"1.175"},{"@class":"com.example.baedanguem.model.Dividend","date":[2017,5,17,0,0],"dividend":"1.175"},{"@class":"com.example.baedanguem.m"[truncated 1906 bytes]; line: 1, column: 115] (through reference chain: com.example.baedanguem.model.ScrapedResult["company"])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.12.5.jar:2.12.5]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764) ~[jackson-databind-2.12.5.jar:2.12.5]

 

(4)  추가적인 내용

- 스프링 프로그램을 종료하더라도, 캐시 서버에는 데이터가 남아있다.

- @Cacheable 메소드 안에 log를 남길 경우, 처음 DB에서 데이터를 조회할 때 콘솔에 로그가 남지만,

  캐시에서 데이터를 조회할 때에는 콘솔에 log가 남지 않는다.

 

4. 레디스 캐시 삭제

- 데이터가 업데이트 되었음에도 캐시에 이전 데이터가 남아다면, 이전 조회기록을 가져오게 된다.

- 따라서, 해당 캐시의 내용을 업데이트하거나, 해당 내용을 삭제하는 것이 필요하다.

- 더불어, 캐시 공간 또한 한계가 있기 때문에 오래된 캐시 데이터는 삭제하는 것이 필요하다.

* 수업에서는, 매일 00시에 데이터를 받아 업데이트를 진행하기 때문에, 스케줄러 메소드에 작업을 했다.

 

(1) @CacheEvict를 사용하여 특정 value 의 데이터를 삭제한다.

- 앞서서 캐시한 조회 메소드에 @Cacheable(key = "#companyName", value = "finance") 설정을 했었다.

- @CacheEvict의 value에 설정했던 "finance"를 넣어주고, allEntries = true 를 하면 "finance"로 캐시된 모든 데이터를 삭제한다.

@CacheEvict(value = CacheKey.KEY_FINANCE, allEntries = true)
@Scheduled(cron = "${scheduler.scrap.yahoo}")
public void yahooFinanceScheduling() {
   // 내용  
}

- 만약, 특정 데이터만 삭제하고 싶다면, key값에 해당 데이터의 key값을 넣어주면 된다.

@CacheEvict(value = CacheKey.KEY_FINANCE, key = "someCompany")
@Scheduled(cron = "${scheduler.scrap.yahoo}")
public void yahooFinanceScheduling() {
   // 내용  
}

 

(2) @CacheEvict를 붙인 스케줄러에 @EnableCaching을 추가한다.

- 스케줄러는 Main 메소드와 다른 쓰레드를 사용한다.

- 스케줄러가 동작할 때마다, @EnableCaching이 이루어질 수 있도록 추가한다.

@Component
@AllArgsConstructor
@EnableCaching
@Slf4j
public class ScraperScheduler {

    @CacheEvict(value = CacheKey.KEY_FINANCE, allEntries = true)
    @Scheduled(cron = "${scheduler.scrap.yahoo}")
    public void yahooFinanceScheduling() {
       // 내용  
    }
    
}

 

(3) TTL (Time to Live) 설정하기

- TTL : 데이터의 유효기간

- 앞에서 CacheConfig작성시 RedisCacheConfiguration을 작성했던 부분에 TTL 기간을 추가한다.

@Configuration
@RequiredArgsConstructor
public class CacheConfig {

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

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

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){

        RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofDays(30)); // TTL 시간 지정 

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(conf)
                .build();
    }

}

 

 

[ 참고 및 출처 ]

부트캠프 내용 정리

Redis란?

https://steady-coding.tistory.com/586

https://www.baeldung.com/spring-boot-redis-cache

Redis 윈도우 10 환경에서 설치 후 실행하기

https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-Window10-%ED%99%98%EA%B2%BD%EC%97%90-Redis-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0