개발 낙서장

Spring + Redis Cache 본문

Java

Spring + Redis Cache

권승준 2024. 5. 13. 18:56

Cache

캐시(Cache)란 데이터를 미리 임시 장소에 저장해 두고 필요할 때 꺼내 쓰는 방법을 말한다.
즉 '조회' 효율을 높이기 위한 기술이다.
원본 데이터에 접근하는 비용이 크거나 지속적으로 비슷한(혹은 같은) 데이터들을 로드하는 경우에 거의 필수적으로 사용한다.

캐시를 사용하지 않으면 10만 개의 글 목록을 API 호출 마다 DB에 접근하여 불러오겠지만 캐싱 처리를 하게 되면 기존 데이터는 별도 메모리에 저장해두고 새로 업데이트 되는 값만 DB에서 불러오면 되기에 매우 빠른 처리 속도를 보여준다.

DB에 직접 접근하는 것보다 메모리에 저장해 캐싱하는 것이 더 빠른 이유는 저장 공간의 차이에 있다.
보통 DB는 SSD 같은 디스크에 저장되는데 이는 영구적인 저장 공간이라서 속도가 느리지만 용량이 훨씬 크고 데이터가 보존되는 특징이 있다.
하지만 메모리는 휘발성 저장 공간으로 컴퓨터(혹은 서버)의 전원이 꺼지면 데이터가 전부 날아간다. 하지만 그만큼 데이터 처리 속도가 빠르다.
캐시는 메모리에 저장되기 때문에 데이터를 접근하는데 있어서 DB보다 매우 빠른 처리 속도를 보여주는 것이다.

Redis

Redis는 인메모리 저장 공간으로 NoSQL DB이다.
메모리 기반으로 동작하기 때문에 매우 빠른 데이터 처리 속도를 보여주며 위에서 언급했던 캐싱 처리에 많이 사용된다.

그리고 Key-Value 형태로 단순한 데이터 구조를 갖고 있으며 String, Bitmaps, Hashes, List, Set, Sorted-Sets 등 다양한 자료 구조를 지원한다. 특히 Sorted-Sets를 사용하면 더 빠르게 데이터를 정렬할 수 있다.

또한 Redis는 싱글 스레드로 설계돼있어 더욱 빠른 처리 속도를 보여준다. 하지만 오버헤드가 큰 데이터를 처리할 때는 그만큼 다른 처리가 지연되니 주의해야 한다.

동시성 제어에서도 Redis를 활용할 수 있다. 분산 시스템에서 여러 시스템이 같은 공유 자원을 동시에 접근할 수 있는데 이때 데이터 정합성 문제가 발생할 수 있기에 동시성 제어가 필요하다.
Redis는 분산 락이라는 방법으로 동시성 제어를 할 수 있다.

Spring + Redis

    public List<CategorySpotResponse> getCategorySpotNotCache(String keyword, Integer page) {

        // api 요청
        JSONArray items = requestSearchApi(keyword, page, 10);

        //dto 변환
        List<CategorySpotResponse> itemDtoList = new ArrayList<>();

        for (Object item : items) {
            CategorySpotResponse itemDto = new CategorySpotResponse((JSONObject) item);
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }
    
    public JSONArray requestSearchApi(String keyword, Integer page, int size) {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
            .fromUriString("https://dapi.kakao.com")
            .path("/v2/local/search/keyword.json")
            .queryParam("query", keyword)
            .queryParam("page", page)
            .queryParam("size", size)
            .encode()
            .build()
            .toUri();

        // 카카오 API 호출 시 필요한 인증 정보
        RequestEntity<Void> requestEntity = RequestEntity
            .get(uri)
            .header("Authorization", "KakaoAK")
            .build();

        ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

        // 반환 데이터 JsonArray변환
        JSONObject jsonObject = new JSONObject(responseEntity.getBody());
        JSONArray items = jsonObject.getJSONArray("documents");
        return items;
    }

카카오 API를 통해 특정 키워드의 장소를 불러오는 API이다.
조회를 시도할 때마다 계속해서 외부 API를 호출하기에 조회 횟수가 많아질 경우 오버헤드가 발생하고 데이터 특성이 가변적이지 않아 계속해서 새로운 데이터를 호출할 필요가 없었다.
따라서 Redis를 이용해 호출할 데이터를 캐싱 처리하였다.

properties, gradle

# Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379 # Redis 기본 포트
    // Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Config

@Configuration
@EnableCaching
public class RedisCacheConfig {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        	// 키 직렬화
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            // 값 직렬화
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            // TTL : 3분
            .entryTtl(Duration.ofMinutes(3L));

        return RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(cf)
            .cacheDefaults(redisCacheConfiguration)
            .build();
    }
}

@Cacheable

    @Cacheable(cacheNames = "getCategorySpot", key = "{#keyword, #page}", cacheManager = "cacheManager")
    public List<CategorySpotResponse> getCategorySpot(String keyword, Integer page) {

        // api 요청
        JSONArray items = requsetSearchApi(keyword, page, 10);

        //dto 변환
        List<CategorySpotResponse> itemDtoList = new ArrayList<>();

        for (Object item : items) {
            CategorySpotResponse itemDto = new CategorySpotResponse((JSONObject) item);
            itemDtoList.add(itemDto);
        }
        return itemDtoList;
    }

Cacheable을 사용하여 먼저 캐시에서 데이터를 찾고 없을 경우 API 호출 로직을 처리하도록 했다.

  • cacheNames : 사용(저장)할 캐시의 이름
  • key : 캐시의 키(위 코드에서는 파라미터로 들어온 keyword와 page로 캐시 키 설정)
  • cacheManage : 사용할 캐시 매니저(Config에서 설정한 Bean)

속도 비교

먼저 기존 방식으로 10번 조회해봤다.

전부 스크린 샷을 찍지는 못했지만 적게 나오면 55ms 많게 나오면 68ms 정도로 찍혔다.

다음으로 Redis 캐싱 처리한 방식으로 똑같이 10번 조회해봤다.

역시 스크린 샷을 전부 찍지는 못했지만 최저 9ms, 최고 13ms 정도로 찍혔다.

평균 61ms -> 11ms로 약 80% 이상의 속도 개선 효과를 보였다.
아마 데이터의 양이 많을 수록 복잡할 수록 차이는 더 벌어질 것이다.

정리

적은 데이터로 테스트했음에도 이렇게 유의미한 성능 개선 효과를 보였다. 캐싱 처리는 데이터 조회에 있어 필수적인 부분이라는 생각이 들었다.

하지만 캐시는 메모리에 저장하기에 용량이 적고 비용이 비싸다. 따라서 모든 기술이 그렇듯 데이터를 잘 분석하여 선택하는 것이 중요하다고 생각한다.

'Java' 카테고리의 다른 글

[Spring] @RequestPart 테스트 HttpMediaTypeNotSupportedException  (0) 2024.05.17
JDK 17을 쓰는 이유  (1) 2024.05.02
[Spring] WebSocket 통신  (0) 2024.03.28
[Spring] AOP로 권한 체크하기  (0) 2024.03.21
[Spring] QueryDSL 페이징  (0) 2024.03.12
Comments