티스토리 뷰

728x90
반응형

적용하게 된 계기

회사내에 프로젝트를 하다가 상품등록시에

상품마다 매번 Category, Brand 등의 테이블을 조회 해와서

유효성 검증을 하고 있었다. 이걸 개선해보고 싶어서 캐싱을 적용해 봤고 포스팅을 해보려고 한다.

 

 

캐시할 데이터

업데이트가 자주 일어나지 않고, 자주 조회하는 Category, Brand, Keyword를 캐시해서 사용하려고 한다. 캐시를 하면 매번 DB에서 조회하지 않고, 빠르고, 네트워크를 타지 않기 때문에(Local cache 한정) 효율적으로 가져올 수 있다고 생각했기 때문이다.

 

 

Local cache VS Global cache

캐시는 크게 Local Cache와 Global Cache로 나눌 수 있는데,

그중 Local Cache를 적용하기로 했다.

Local Cache를 쓴 이유 :

  1. Global Cache와 비교하여 속도가 빠르다.
  2. 네트워크를 타지 않는다.

 

Cache Manager

Local cache중에서 cache manager는 ehcache 3를 사용했다.

Ehcache3를 쓴 이유:

  1. ehcache 3버전 부터 JSR-107 cache manager를 구현했다.
  2. 그렇기 때문에 나중에 redis등의 global cache를 사용하더라도 구현체만 바꾸면 코드의 큰 수정없이 쉽게 변경 가능하다.
  3. JMX를 이용한 모니터링 가능

 

캐시를 사용하면서 주의할 점.

  • 코드를 보고 아래에서 설명하려고 한다!

github에 올린 예제 코드

https://github.com/hoon7566/blog-code/tree/main/spring-boot-cache

 

Gradle

implementation("org.springframework.boot:spring-boot-starter-cache:2.3.3.RELEASE")
implementation("javax.cache:cache-api:1.1.1")
implementation("org.ehcache:ehcache:3.8.1")

Config 코드

object CacheNames {
  const val META_DATA_SERVICE_GET_CATEGORY: String = "cacheservice.category"
}

data class CacheSetting(
  val cacheNames: String,
  val config: MutableConfiguration<Any, Any>
)

@EnableCaching
@Configuration
class CacheConfig {

  @Bean
  fun customCacheManager( cacheSettings : List<CacheSetting>): CacheManager {
    val cacheManager: CacheManager = Caching.getCachingProvider().cacheManager

    cacheSettings.forEach { cacheSetting ->
      cacheManager.createCache(cacheSetting.cacheNames, cacheSetting.config)
    }
    return cacheManager
  }

  @Bean
  fun getCategoryCache() : CacheSetting {
    return CacheSetting(
      CacheNames.META_DATA_SERVICE_GET_CATEGORY,
      MutableConfiguration<Any, Any>()
        .setTypes(Any::class.java, Any::class.java)
        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.TEN_MINUTES))
        .setStoreByValue(true) 
//        .setReadThrough(true) // 캐싱 전략
//        .setWriteThrough(true) // 캐싱 전략
    )
  }

}

Spring의 버전이 올라가면서 xml이 아닌 java config가 권장되고 있으므로 java config로 해보았다.

Service 코드

@Service
class CacheService {

  @Cacheable(value = [CacheNames.META_DATA_SERVICE_GET_CATEGORY], key = "#root.methodName")
  fun getCategory() : List<CategoryDto> {
    return mutableListOf(
        CategoryDto("11111111", "반팔" ),
        CategoryDto("11111112", "반바지" ),
        CategoryDto("11111113", "코트" ),
    )
  }
}

Local Cache 객체 변조 가능성?

local cache를 사용하고 있으므로 가져온 값을 변경하게 되면 객체가 변경되므로 고려해야 한다.

이를 방지하기 위한 방법으로

  1. immutable한 객체를 캐시하여 사용함.
  2. 캐시의 값을 copy하여 사용함.

1번은 코드를 작성 할 떄 주의해서 작성하는 부분이고,

2번은 설정값을 통해 간단하게 적용 가능하다.

위 config 코드중에 setStoreByValue(true)값이 있는데, 이를 false로 설정한다면 다음과 같이 local cache값이 변경될 수 있다.

변경되는 예시에 대해서 확인해보자.

예시

다음과 같은 코드가 있다고 했을 때

@RestController
class CacheController(
  private val cacheService: CacheService
) {

  @GetMapping("/category")
  fun getCategory() =
    ResponseEntity.ok(cacheService.getCategory())

  @GetMapping("/test-modify-category")
  fun testModifyCategory(): ResponseEntity<List<CategoryDto>> {
    val categoryList = cacheService.getCategory()

    categoryList.find { categoryDto -> categoryDto.categoryCode == "11111111" }?.let {
      it.categoryName = "반팔 카테고리명 변경 테스트"
    }
    return ResponseEntity.ok(categoryList)
  }
}

/test-modify-category 엔드포인트를 호출하고 나서 /category를 호출했을 때 다음과 같이 캐시값이 변조되어버린다.

[
    {
        "categoryCode": "11111111",
        "categoryName": "반팔 카테고리명 변경 테스트"
    },
    {
        "categoryCode": "11111112",
        "categoryName": "반바지"
    },
    {
        "categoryCode": "11111113",
        "categoryName": "코트"
    }
]

하지만 설정값중 setStoreByValue(true)를 하면 기존 값에서 변경되지 않는 것을 확인할 수 있다.

[
    {
        "categoryCode": "11111111",
        "categoryName": "반팔"
    },
    {
        "categoryCode": "11111112",
        "categoryName": "반바지"
    },
    {
        "categoryCode": "11111113",
        "categoryName": "코트"
    }
]

주의할 점 

캐시를 사용하면서 주의할 점으로 위에서 말한 캐시 데이터 변조와 Cache Stampede를 조심하여야 할 것으로 보인다.

캐시를 사용할 때는 주의할 점이 Cache가 만료되는 시점에 요청이 몰리게 된다면 cache stampede현상이 일어날 수 있고, 데이터베이스 부하(Thundering Herd)가 일어날 수 있다.

그래서 캐싱 전략 패턴을 확인하고 데이터에 맞는 패턴을 적용하는 것이 중요하다.

 

추가할 것

추후에 캐시할 데이터가 갱신될 때 단순히 TTL설정뿐만이 아닌 캐시대상 table에 변경이 일어났을시에 cdc를 통해서 캐시를 갱신할 수 있도록 하는 것을 포스팅 해보려고 한다.

 

 

 

 

 

 

 

참고 : https://inpa.tistory.com/entry/REDIS-📚-캐시Cache-설계-전략-지침-총정리

728x90
반응형
250x250
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함