| 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
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 환경에서 설치 후 실행하기
'Infra > Redis' 카테고리의 다른 글
[Redis] 스프링에서 Redis Cache에 Data Crud 하는 방법 (0) | 2022.12.12 |
---|---|
[Docker/Redis] Ubuntu에 Redis 설치 & Docker로 Redis 실행 (0) | 2022.12.09 |
[Redis] Redis Insights 사용하기 (0) | 2022.11.24 |
Redis에 대한 정보 모음 (0) | 2022.10.20 |