이전 포스팅에서 api하나를 분리하고 2개의 api로 만든 것을 확인할 수 있었다.
그리고 마지막으로 cache 적용기를 메인 주제로 하여 2탄을 가져왔다.
cache를 도입하려고 하는 이유가 무엇일까?
6시에 한번 Update하고 그 이후로는 계속 동일한 정보에 대해서 굳이 database에서 계속 조회할 필요가 없다. cpu는 memory보다 cache를 더 빨리 읽기 때문이다. 그리고 home page에 접근할 때마다 계속 필요한 데이터이다. 하루에 사용자 1명당 발생할 수 있는 트래픽이 가장 많을 페이지이다. 그렇다는 것은 더더욱 데이터베이스에 가면 안 된다.
spring에서 사용이 가능한 cache에는 여러가지가 존재한다.
redis는 다양한 형태로 cache를 사용해 데이터를 저장할 수 있다는 장점이 있다.
그리고 가용성이 뛰어난 인 메모리 캐시 구현에 매우 적합하다고 한다.
그래서 redis를 사용해보기로했다.
docker image로 불러오자!
version: "3.8"
networks:
application:
driver: bridge
services:
redis:
image: redis:latest
container_name: redis
ports:
- 6379:6379
volumes:
- ./redis/data:/data
- ./redis/conf/redis.conf:/usr/local/conf/redis.conf
labels:
- "name=redis"
- "mode=standalone"
restart: always
command: redis-server /usr/local/conf/redis.conf
바로 terminal에서 실행해도 되지만, application 실행할때마다 하면 귀찮다!! docker compose file로 관리하기로 했다.
docker compose up
해당 명령어로 간단하게 container를 띄울 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
의존성을 등록해준다.
config에 @EnablaeCaching을 사용해서 spring이 redis를 사용할 수 있도록해준다.
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofHours(24L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
}
}
cache삭제를 24시간 주기로 해줬다. 외부 api를 24시간에 한번씩 call하고 저장할 것이기 때문이다.
@Cacheable(value = "weather", key="#informType")
public ApiInfoDto getApiDataByInformType(@PathVariable InformType informType, ApiInfoDto apiInfoDto) throws IOException, ParseException {
log.info("no api info for {}", informType);
switch (informType){
case PARTICULATE:
HashMap<String, String> particulate = updateApiDataParti();
return apiInfoDto.updateParticulate(particulate.get("informCause"), particulate.get("informOverall") );
case WEATHER:
HashMap<String, String> weather = updateApiDataWeather();
return apiInfoDto.updateWeather(weather.get("informSky"),weather.get("informPty") );
}
return apiInfoDto;
}
@CacheEvict(value="weather", allEntries = true)
public void deleteApiDataAll(){
}
key, value쌍으로 구성된 weather라는 이름을 가지는 cache에 key(informType)에 대한 value값이 존재한다면 getApiDataByInformType 안의 method를 실행시키지 않고 바로 cache의 key안의 value를 반환해준다.
allEntries를 true로 하여 cache를 삭제한다. key value를 모두 삭제해줄 것이다.
@Transactional
public HashMap<String, String> updateApiDataParti() throws IOException, ParseException {
HashMap<String,String> particulatePredictInfo = particulateMatter.extractParticulatePredictInfo();
return particulatePredictInfo;
}
@Transactional
public HashMap<String, String> updateApiDataWeather() throws IOException, ParseException {
List<City> cityList = cityRepository.findAll();
HashMap<String, String> weatherDataForAllCity = getWeatherDataForAllCity(weather, "", "", cityList);
return weatherDataForAllCity;
}
특정 시간이 되면(6시로 설정) 외부 api값을 불러와야하는데 2개의 method를 사용해야한다.
미세먼지, 날씨 데이터를 각각 가져온다.
@Scheduled(cron = "0 0 6 * * *")
public void updateApiData() throws IOException, ParseException {
log.info("update data Scheduled");
//이전의 cache를 모두 삭제
deleteApiDataAll();
ApiInfoDto partiApiInfoDto = getApiDataByInformType(InformType.PARTICULATE, new ApiInfoDto());
getApiDataByInformType(InformType.WEATHER, partiApiInfoDto);
}
cache에 6시가 됐을 때 api에서 data를 불러오고 초기화해주는 과정이전에, 기존에 존재하는 cache에서 저장중이였던 데이터들을 모두 삭제해준다.
이렇게 완성했다면 이제 controller에서 적용하면 된다!
@GetMapping("/weather")
public ApiInfoDto getWeather() throws IOException, ParseException {
//cache를 조회 -> 존재하지 않을 때 아래를 실행. -> cache를 생성
log.info("getWeather controller");
ApiInfoDto partiApiInfoDto = apiMapService.getApiDataByInformType(InformType.PARTICULATE, new ApiInfoDto());
ApiInfoDto apiInfoDto = apiMapService.getApiDataByInformType(InformType.WEATHER, partiApiInfoDto);
return apiInfoDto;
}
기존의 api에서 weather라는 api를 따로 빼서 하나의 역할만 가지도록 했다(이전 포스팅 참고).
아예 api data에 대해서는 database를 사용하지 않도록 변경했다. 따라서 기존에는 memory를 차지하고 있었던 ApiData entity에 대한 table이 없어졌고 관련된 코드 역시 모두 불필요해졌다.
100명이 100번 요청 -> 3회 반복
TPS 즉, Throughput의 경우 약 3배 정도 차이가 나는 것을 확인할 수 있었다.