[spring] (2탄 : redis cache 적용) 원점으로 돌아가서 api를 갈아 엎어보자!

sujin·2023년 7월 23일
2

spring

목록 보기
8/13
post-thumbnail

intro. 이전 포스팅에 이어서..

이전 포스팅에서 api하나를 분리하고 2개의 api로 만든 것을 확인할 수 있었다.
그리고 마지막으로 cache 적용기를 메인 주제로 하여 2탄을 가져왔다.

cache를 도입하려고 하는 이유가 무엇일까?

6시에 한번 Update하고 그 이후로는 계속 동일한 정보에 대해서 굳이 database에서 계속 조회할 필요가 없다. cpu는 memory보다 cache를 더 빨리 읽기 때문이다. 그리고 home page에 접근할 때마다 계속 필요한 데이터이다. 하루에 사용자 1명당 발생할 수 있는 트래픽이 가장 많을 페이지이다. 그렇다는 것은 더더욱 데이터베이스에 가면 안 된다.

  • 그래서?
    그래서 나는 cache를 도입해서 성능을 upgrade시켜보기로했다.
    물론,,,,엄청나게 많은 데이터가 아니여서 드라마틱한 차이가 없을 것은 예상했다.(한번의 request에 대해서 말이다!)
    그런데,,,동시에 많은 양의 요청이 있다면 main page이기 때문에 사용자가 답답함을 느낄 수 있다고 생각했다.

1. redis

redis image

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를 띄울 수 있다.

dependency

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

의존성을 등록해준다.

config

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하고 저장할 것이기 때문이다.

2. api에 적용하기

service

@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(){

}
  • Cacheable
    weather라는 cache에서 informType값에 따른 (key) value를 초기화해주고 값이 반환된다.

key, value쌍으로 구성된 weather라는 이름을 가지는 cache에 key(informType)에 대한 value값이 존재한다면 getApiDataByInformType 안의 method를 실행시키지 않고 바로 cache의 key안의 value를 반환해준다.

  • cacheEvict

allEntries를 true로 하여 cache를 삭제한다. key value를 모두 삭제해줄 것이다.

api call service 그리고 scheduler

@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에서 적용하면 된다!

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를 따로 빼서 하나의 역할만 가지도록 했다(이전 포스팅 참고).

3. 결과

  • database에 저장하지 않음

아예 api data에 대해서는 database를 사용하지 않도록 변경했다. 따라서 기존에는 memory를 차지하고 있었던 ApiData entity에 대한 table이 없어졌고 관련된 코드 역시 모두 불필요해졌다.

  • 기존의 api에서 weather api관련 service만 적용한 api와 cache를 적용한 weather api service에 대한 api를 비교해보자

100명이 100번 요청 -> 3회 반복

TPS 즉, Throughput의 경우 약 3배 정도 차이가 나는 것을 확인할 수 있었다.

0개의 댓글