카테고리 없음

2024-07-04 [Kotlin+Spring In-memory Cache(@Cacheable)]

Glen_check 2024. 7. 4. 21:46

Cache를 왜 사용하는 걸까?

이번 팀 프로젝트의 주제는 "Cache를 이용한 성능 개선 프로젝트"입니다.

사실 Cache Project를 진행하기 이전부터 튜터님의 피드백이나 세션 때 애플리케이션 성능 개선에 대한 많은 언급들이 있었기 때문에 중요한 것은 잘 알고 있었으나, 몸소 체감되지는 않았습니다.

 

체감되지 않는 이유를 생각해 보면,

1. 현재까지 규모가 크지 않은, 비교적 단순한 프로젝트를 진행하다 보니 성능 개선에 대한 필요성을 느낄 수 없었고,

2. 현실적인 나의 현 단계에서는 성능보다 문제 해결, 즉 목표하고자 하는 기능을 구현하는 것이 중요했습니다.

그런데 이제는 아주 조금은 알 것 같습니다. 성능을 개선하는 것이 개발자의 지속적인 고민거리이자 과제이며, 곧 문제 해결 능력이라는 것을요 ..

 

정리해 보면, 사용자가 많은 애플리케이션을 관리하는 관리자라고 가정해 봅시다!
무수한 유저들의 많은 요청을 처리하기 위해 지속적으로 DB를 들여다봐야 한다면? 한 번의 요청에도 많은 데이터들을 필요로 한다면 문제는 발생할 수밖에 없습니다.
관리자는 다음의 문제를 해결하기 위해 Cache를 사용하는 건데요, 그러면 Cache가 정확히 무엇일까요?!!

 

Cache가 뭘까?

Cache는 하나의 메모리 공간을 의미하는데요, 메모리는 속도가 빠른 장치와 느린 장치 간 속도 차에 따른 병목 현상을 줄이기 위한 범용 메모리입니다.

캐시 메모리는 주기억장치에서 자주 사용하는 프로그램과 데이터를 저장해두어 처리 속도를 빠르게 하여 성능을 개선하는데 활용됩니다.
여기서 Cache 기억장치와 주기억장치 사이에서 정보를 옮기는 것을 Mapping이라고 합니다.

정리해보면, 자주 사용되는 데이터들을 Cache Memory에 저장하면 성능 개선에 큰 도움이 되겠죠?!

 

(Cache에 대해 찾아보면, 정말 많은 정보들이 저장되어 있지만 오늘은 실질적인 사용 방법에 대해 정리하고자 캐시의 지역성(Cache Locality)만 추가로 짚어보고 넘어가려 합니다!)

 

Cache Locality (캐시의 지역성)

위에서 잠깐 언급한 것처럼 캐시 메모리의 역할을 제대로 수행하기 위해서는 'CPU가 어떤 데이터를 자주 들여다볼지' 어느 정도 예측할 수 있어야 합니다. 캐시의 성능은 작은 용량의 캐시 메모리에 CPU가 이후에 참조할, 쓸모 있는 정보가 어느 정도 들어있느냐에 따라 좌우되기 때문인데요, 이 때 적중율(Hit rate)을 극대화 시키기 위해 데이터 지역성(Locality)를 사용합니다.

지역성의 전제 조건으로 프로그램은 모든 코드나 데이터를 균등하게 Access 하지 않는다는 특성을 기본으로 합니다. 즉, Locality란 기억 장치 내의 정보를 균일하게 Access 하는 것이 아닌, 어느 한 순간에 특정 부분을 집중적으로 참조하는 특성인 것입니다!

지역성을 고려하여 우리는 캐시를 사용해야 할 상황을 인지할 수 있습니다.

시간적 지역성(Temporal Locality)

시간적 지역성은 최근 참조된 주소의 내용은 곧 다음에 다시 참조되는 특성을 의미합니다.

메모리 상의 같은 주소에 여러 차례 읽기 쓰기를 수행할 경우 상대적으로 작은 크기의 캐시를 사용해도 높은 효율성을 꾀할 수 있습니다.

공간적 지역성(Spatial Locality)

공간적 지역성은 기억장치 내 서로 인접하여 저장되어 있는 데이터들이 연속적으로 액세스 될 가능성이 높아지는 특성을 의미합니다.

CPU 캐시나 디스크 캐시의 경우, 한 메모리 주소에 접근할 때 그 주소뿐 아니라 해당 블록을 전부 캐시에 가져오게 된다.

이때 메모리 주소를 오름차순이나 내림차순으로 접근한다면, 캐시에 이미 저장된 같은 블록의 데이터를 접근하게 되므로 캐시의 효율성이 크게 향상될 수 있습니다.

스프링에서 제공하는 기본 Cache (In-Momory Cache)

Spring Application에서도 In-Memory를 활용하여 캐시를 쉽게 추가할 수 있도록 도움을 주는 기능을 제공합니다.

유사 Transaction을 지원하고, 사용하고 있는 코드(메소드)에 영향을 최소화하면서 일관된 방법으로 캐시를 사용 할 수 있습니다.

Spring에서 캐시 추상화는 메소드를 통해 기능을 지원하는데, 메소드가 실행되는 시점에 파라미터에 대한 캐시 존재 여부를 판단하여 없으면 캐시를 등록하게 되고, 캐시가 있으면 메소드를 실행시키지 않고 캐시 메모리 안 데이터를 Return 해주게 됩니다.

Spring Cache 추상화를 지원하기 때문에 개발자는 별도의 캐시 로직을 작성하지 않아도 되지만 캐시를 저장하는 저장소는 직접 설정을 해줘야 합니다. Spring에서는 CacheManager라는 Interface를 제공하여 캐시를 구현할 수 있도록 도움을 주고 있습니다.

 

CaffeineCacheManager 활용 Cache 구현 (Spring + Kotlin)

'인기검색어 Top10'을 조회할 수 있는 메소드에 Cache를 적용해보았는데요!

해당 메소드에 Cache를 적용한 이유를 생각해보면 일반적인 커뮤니티 사이트는 게시글을 작성하는 사람들보다 검색어를 통해 원하는 Post를 조회하는 경우가 더 많습니다. 그리고 인기검색어는 실시간으로 큰 변동이 있는 항목이 아닌데, 매번 주기억장치에서 데이터를 꺼내오는 것은 매우 비효율적이라고 생각하여 해당 메소드에 Cache를 적용해보았습니다.

 (Cache의 만료 시간(TTL)을 설정하기 위해 CacheConfig class에서 CaffeineCacheManager를 상속받아 사용해봤습니다. 캐시는 등록된지 1분 후에 만료될 수 있도록 설정하였습니다.)

 

(1-1) Gradle 설정

    implementation ("org.springframework.boot:spring-boot-starter-cache")
    implementation ("com.github.ben-manes.caffeine:caffeine")

다음 의존성을 설정하면, SpringCache를 사용할 준비가 완료되었습니다!

(아래의 의존성은 Caffeine 기능도 함께 활용하기 위해 추가적으로 활용했습니다.)

 

(1-2) CacheConfig (@EnableCaching)

@Configuration
@EnableCaching
class CacheConfig {

    @Bean("caffeineCacheManager")
    fun caffeineCacheManager(): CaffeineCacheManager {
        val cacheManager = CaffeineCacheManager()

        val caffeine = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES)

        cacheManager.setCaffeine(caffeine)
        cacheManager.setCacheNames(listOf("hotKeywordsLastHour", "hotKeywordsLastDay")

        return cacheManager
    }
}

@EnableCaching Annotation을 등록해줍니다. Application에 활용해도 무방합니다.

CaffeineCacheManager는 CacheManger Interface를 상속받은 Public Class로,

1. (고성능)Caffeine은 최적화된 캐시 구현을 통해 높은 성능을 제공합니다. 예를 들어, eviction 정책이나 통계 수집 등의 기능이 매우 빠르게 동작할 수 있도록 도움을 주며,

2. (유연한 캐시구성)다양한 eviction 정책 (예: LRU, LFU, FIFO 등)을 지원하며, 만료 시간 (TTL) 등을 세부적으로 설정할 수 있습니다.

캐시 크기 제한, 만료 정책, 리로딩 전략 등을 유연하게 구성할 수 있다는 장점이 있습니다! 

 

코드의 주요 부분을 살펴보면,

Caffeine.newBuilder()

Caffeine Class의 newBuilder 메서드는 캐시 설정을 위한 빌더를 생성합니다! 빌더를 사용하여 캐시의 다양한 속성을 구성할 수 있습니다.

expireAfterWrite(1, TimeUnit.MINUTES)

이 설정은 캐시에 저장된 항목이 쓰여진 후 1분 동안만 유효하도록 합니다. 1분이 지나면 캐시 항목이 만료되어 제거됩니다.

해당 설정은 데이터가 자주 업데이트 되거나 변동이 심한 데이터를 캐시에 저장할 때 유용합니다. 데이터가 오래되기 전에 갱신되거나 제거되어야 할 경우 'expireAfterWrite' 설정을 통해 캐시 항목의 유효 기간을 짧게 설정하여 데이터를 최신 상태로 유지할 수 있습니다!

 

cacheManager.setCacheNames(listOf("hotKeywordsLastHour", "hotKeywordsLastDay"))

이 메서드는 CaffeineCacheManager가 관리할 캐시의 이름을 설정합니다. 여기서는 "hotKeywordsLastHour"와 "hotKeywordsLastDay"라는 두 개의 캐시를 설정합니다.

 

(1-3) 메소드에 Cache 적용하기

@Cacheable("hotKeywordsLastHour")
    fun getHotKeywordsLastHour(): List<String> {
        val now = LocalDateTime.now()
        val from = now.minusHours(1)
        return keywordRepository.getHotKeywords(from, now)
    }

    @Cacheable("hotKeywordsLastDay")
    fun getHotKeywordsLastDay(): List<String> {
        val now = LocalDateTime.now()
        val from = now.minusDays(1)
        return keywordRepository.getHotKeywords(from, now)
    }

조회하는 시점으로부터 한시간 전까지 데이터를 종합하여 인기검색어를 조회하는 getHotKeywordsLastHour,

하루동안의 데이터를 종합하여 인기검색어를 조회하는 getHotKeywordsLastDay Method에 @Cacheable Annotation을 통해 Cache를 정상적으로 활용할 수 있습니다.

 

(**혹시나 잘못된 내용이나, 추가적으로 설명이 필요한 부분이 있다면 댓글 부탁드리겠습니다!!!!)