1. 문제
이번 7주차의 과제는 다음과 같다.
* 캐싱을 적용하여 성능을 개선할 수 있는 로직을 분석하고, 이를 합리적인 이유와 함께 문서 정리하기
* 적절한 캐싱 전략을 적용한 비즈니스 로직 작성하기
이번 주차에서는 드디어 캐싱에 대해 다루게 되었다. 사실 지금까지 실무에서 캐싱에 대해 다뤄본 적이 없었기 때문에 어떤 부분부터 분석해야 할지 감이 잡히지 않았다. 그래서 우선 캐싱에 대해 더 자세히 알 필요가 있었다. 그래서 찾아본 내용들은 다음과 같았다.
- 캐시 및 캐싱이란?
- 캐싱을 적용해야 하는 경우
- 캐싱 적용할 때 주의사항
위 살펴본 내용들을 바탕으로 나의 시나리오 로직들을 분석해보고 캐싱을 적용하기 적절한지 판단하는 게 주 과제였다.
2. 시도
앞에서 이야기했던 캐싱에 대해 찾아본 내용을 더 자세히 다뤄보자면 다음과 같다.
- 캐시 및 캐싱이란?
- 캐시: 사용자 입장에서 데이터를 더 빠르고 효율적으로 접근할 수 있는 임시 데이터 저장소. DB에 접근하는 속도보다 빠른 저장소를 사용하는 것이 일반적이며, 보통 메모리를 사용하는 저장소를 사용한다.
- 캐싱: 시스템 성능 향상을 위한 기술로 캐시는 메모리를 사용하기 때문에 디스크 기반의 DB보다 더 빠르게 데이터에 접근할 수 있어, 더 빠르게 서비스를 제공하게 해줄 수 있는 주요한 기술이다.
- 캐싱을 적용해야 하는 경우
- 동일한 데이터에 반복적으로 접근하는 상황이 많을 때, 즉 데이터 재사용 횟수가 한 번 이상이어야 캐싱의 의미가 있다.
- 잘 변하지 않는 데이터일수록 캐싱을 사용하는 것이 효과적이다.
- 데이터에 접근할 때 복잡한 로직이 필요한 경우 사용하는 것이 효과적이다.
- 캐싱 적용할 때 주의사항
- 캐시는 주로 데이터를 휘발성 메모리에 저장하기 때문에 영구적으로 보관되어야 하는 데이터를 캐시에 저장하는 것은 바람직하지 않다.
- 적절한 만료 정책이 필요하다. 만료된 데이터는 캐시 저장소에서 삭제되어야 한다. 만료 정책이 없다면 데에터가 계속 캐시에 남아있어 메모리 용량이 부족해지는 상황이 올 수 있기 때문이다.
이렇게 캐싱에 대해 찾아본 내용을 기반으로 시나리오의 로직을 분석해봤다. 그렇게 캐싱 적용이 필요할 것으로 선택한 로직은 두 가지로 추려졌다.
- 주문 상위 Top5 상품 통계 데이터
- 통계 데이터를 조회할 때 비교적 복잡한 쿼리 연산이 필요하다.
-
@Query("SELECT new com.example.hhplus_ecommerce.domain.order.dto.OrderQuantityStatisticsInfo(oi.productDetailId, SUM(oi.quantity)) " + "FROM OrderItem oi " + "WHERE oi.createdDate >= :standardDate " + "GROUP BY oi.productDetailId " + "ORDER BY SUM(oi.quantity) DESC") fun findTopQuantityByCreatedDateMoreThan(@Param("standardDate") standardDate: LocalDateTime, pageable: Pageable): List<OrderQuantityStatisticsInfo>
-
- 모든 사용자들이 조회해볼 수 있는 데이터이므로, 반복적으로 접근하는 데이터로 판단했다.
- 통계 데이터를 조회할 때 비교적 복잡한 쿼리 연산이 필요하다.
- 상품 메인 정보 데이터
- 통계 데이터와 마찬가지로 모든 사용자들이 조회해볼 수 있는 데이터이므로, 반복적으로 접근하는 데이터로 판단했다.
- 상품 메인 정보는 잘 변하지 않는 데이터이다.
3. 해결
먼저 캐싱 전략은 Redis의 인메모리 캐시 환경을 사용했다. 이를 위해 Redis와 캐시에 관한 설정을 해줬다.
@Configuration
class RedisConfig {
@Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> {
val template = RedisTemplate<String, Any>()
template.setConnectionFactory(connectionFactory)
template.keySerializer = StringRedisSerializer()
template.valueSerializer = GenericJackson2JsonRedisSerializer()
return template
}
}
@Configuration
@EnableCaching
class CacheConfig {
@Bean
fun redisCacheManager(redisConnectionFactory: RedisConnectionFactory): RedisCacheManager {
val cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(10)) // 기본 TTL 10분
val cacheConfigurationMap = mapOf(
// 상위 주문 상품 통계 캐시 TTL 5분
"topOrderProductStatistics" to RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)),
"productInfo" to RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
)
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.withInitialCacheConfigurations(cacheConfigurationMap)
.build()
}
}
환경 구성에서 어려웠던 점은 Redis에 담을 Class Type 데이터의 직렬화가 잘 안돼서 직렬화 관련 이슈를 잡는 데 조금 시간을 소요했다. CacheConfig
에서 GenericJackson2JsonRedisSerializer()
로 설정하면 Clas Type인 데이터가 직렬화 될 줄 알았는데, 그래도 캐시에 데이터를 저장할 때 직렬화 관련 에러가 계속 발생했다. 해결 방법을 더 찾아보니 저장할 Class Type에 Serializable
인터페이스를 구현하도록 해서 해결했다. (이럴거면 GenericJackson2JsonRedisSerializer()
는 왜 설정해준걸까? 이는 더 찾아봐야겠다…😥)
class OrderProductStatisticsResponseItem(
val productId: Long,
val name: String,
val totalSold: Int
) : Serializable {
// ...
캐싱 관련 환경 구성을 해준 다음 Spring 프레임워크에서 제공해주는 @Cacheable
어노테이션을 통해 캐싱을 적용해줬다. @Cacheable
어노테이션을 메서드에 정의해주면 해당 메서드에서 반환되는 데이터는 @Cacheable
에서 설정한 key
값과, CacheConfig
의 redisCacheManager()
환경설정에서 TTL 등과 같이 value
값에 따라 설정한 내용으로 캐시에 저장된다.
아래는 주문 상위 Top5 상품 통계 데이터에 캐싱을 적용한 예시이다.
@Cacheable(value = ["topOrderProductStatistics"], key = "#day.toString() + '_' + #limit.toString()", cacheManager = "redisCacheManager")
fun getTopOrderProductStatistics(day: Int, limit: Int): List<OrderProductStatisticsResponseItem> {
// 특정 일자 (day) 내 주문량이 가장 많은 Top (limit) 주문 정보 조회
val orderItemStatisticsInfos = orderService.getAllOrderItemsTopMoreThanDay(day, limit)
val productDetailIds = orderItemStatisticsInfos.map { it.productDetailId }
// 주문 정보에서 상품 목록 조회
val productStatisticsInfos = productService.getAllProductStatisticsInfos(productDetailIds)
return OrderProductStatisticsResponseItem.listOf(productStatisticsInfos, orderItemStatisticsInfos)
}
4. 알게된 것
이번 과제에서 만족스러운 부분은 캐싱에 대해 자세히 알아보고 다뤄볼 수 있는 기회였다는 점이다. 이제 캐싱을 언제 어떤 데이터에 적용해야 하며, 어떨 때엔 지양해야 하는지 판별할 수 있는 지식을 얻을 수 있어 좋았다😊
아직 실무에서 제대로 경험해보지 못한 부분이었기도 해서 시작하기 전에 기대 반 걱정 반이었는데, 스스로 부족하다고 생각하던 점을 잘 충족시켜 준 시간이었다👍
Keep : 현재 만족하고 계속 유지할 부분
처음 제대로 알아보고 다뤄 본 주제였음에도 주어진 기간 내에 무사히 과제를 진행했다는 점은 스스로도 만족스럽고 남은 과제도 지금까지 했던 것처럼만 잘 해쳐나아겠다!
Problem : 개선이 필요하다고 생각하는 문제점
이제 과제에 보고서처럼 문서를 작성하는 과제들이 있다보니 내용들을 정리하고 문서를 작성하는 능력이 많이 필요하다고 느꼈다.📝
Try : 문제점을 해결하기 위해 시도해야 할 것
글을 잘 쓰려면 글을 많이 읽어야한다는 말을 어디서 들은 적이 있다. 다른 잘 작성한 글들을 많이 보면서 글을 잘 쓰는 능력을 더 향상시킬 수 있어야겠다!
이번 과제들도 무사히 통과해서 새로운 뱃지, 브라운 뱃지를 얻을 수 있었다! 사실 이번 과제는 구현한 내용이 캐싱을 적용한 점 뿐으로 많지 않았고, 캐싱에 대해 이해하고, 분석하고 정리하는 내용이었어서, ‘구현한 게 잘 동작하는지’ 보다는 ‘내가 작성한 내용에서 부족한 부분이 있을까?’ 라는 생각으로 과제를 했는데 다행이 좋은 결과를 얻을 수 있었다.
그리고 코치님께 내 기준 최고의 칭찬인 코멘트를 할 부분이 없을 정도로 잘했다는 피드백까지 받았다! 생각 외로 무사히, 아니 완벽하게 마무리한 주차였다 🤩
🤩 다음 수료생 추천 할인 혜택!
혹시라도 항해 플러스에 합류하고 싶은데 비싼 수강료 때문에 망설여진다면…? 🤔
수료생 추천 할인 혜택으로 20만 원을 할인받으실 수 있다는 사실! 💡
결제페이지 → 할인 코드 → 수료생 할인 코드에 tJQjYK 입력하면 추가로 20만 원을 할인받는 혜택 꼭 챙겨가시길 바란다🚀🚀🌟
#추천인: tJQjYK #항해플러스 #항해99