Skip to content

Commit

Permalink
test: 캐시 테스트
Browse files Browse the repository at this point in the history
  • Loading branch information
belljun3395 committed Jan 15, 2025
1 parent 8d40bfa commit 59eb065
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 94 deletions.
29 changes: 29 additions & 0 deletions api/src/main/kotlin/com/few/api/config/ApiLocalCacheConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.few.api.config
import com.few.api.domain.common.repo.event.ApiLocalCacheEventListener
import io.github.oshai.kotlinlogging.KotlinLogging
import org.ehcache.config.builders.CacheConfigurationBuilder
import org.ehcache.config.builders.ExpiryPolicyBuilder
import org.ehcache.config.builders.ResourcePoolsBuilder
import org.ehcache.config.units.EntryUnit
import org.ehcache.event.EventType
Expand All @@ -14,6 +15,7 @@ import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.jcache.JCacheCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Duration

@Configuration
@EnableCaching
Expand All @@ -25,6 +27,9 @@ class ApiLocalCacheConfig {
const val SELECT_ARTICLE_RECORD_CACHE = "selectArticleRecordCache"
const val SELECT_WORKBOOK_RECORD_CACHE = "selectWorkBookRecordCache"
const val SELECT_WRITER_CACHE = "selectWritersCache"
const val ARTICLE_MAIN_CARD_RECORD_CACHE = "articleMainCardRecordCache"
const val ARTICLE_CONTENT_CACHE = "articleContentCache"
const val SELECT_ARTICLE_VIEWS_RECORD_SERVICE_CACHE = "selectArticleViewsRecordServiceCache"
}

@Bean(LOCAL_CM)
Expand Down Expand Up @@ -63,6 +68,18 @@ class ApiLocalCacheConfig {
).withService(cacheEventListenerConfigurationConfig)
.build()

val cache10MinuteConfiguration =
CacheConfigurationBuilder
.newCacheConfigurationBuilder(
Any::class.java,
Any::class.java,
ResourcePoolsBuilder
.newResourcePoolsBuilder()
.heap(200, EntryUnit.ENTRIES),
).withService(cacheEventListenerConfigurationConfig)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
.build()

val selectArticleRecordCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Eh107Configuration.fromEhcacheCacheConfiguration(cache10Configuration)
val selectWorkBookRecordCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Expand All @@ -71,10 +88,22 @@ class ApiLocalCacheConfig {
val selectWriterCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Eh107Configuration.fromEhcacheCacheConfiguration(cache5Configuration)

val articleContentCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Eh107Configuration.fromEhcacheCacheConfiguration(cache10MinuteConfiguration)

val articleMainCardRecordCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Eh107Configuration.fromEhcacheCacheConfiguration(cache10MinuteConfiguration)

val selectArticleViewsRecordServiceCacheConfig: javax.cache.configuration.Configuration<Any, Any> =
Eh107Configuration.fromEhcacheCacheConfiguration(cache10MinuteConfiguration)

runCatching {
cacheManager.createCache(SELECT_ARTICLE_RECORD_CACHE, selectArticleRecordCacheConfig)
cacheManager.createCache(SELECT_WORKBOOK_RECORD_CACHE, selectWorkBookRecordCacheConfig)
cacheManager.createCache(SELECT_WRITER_CACHE, selectWriterCacheConfig)
cacheManager.createCache(ARTICLE_CONTENT_CACHE, articleContentCacheConfig)
cacheManager.createCache(ARTICLE_MAIN_CARD_RECORD_CACHE, articleMainCardRecordCacheConfig)
cacheManager.createCache(SELECT_ARTICLE_VIEWS_RECORD_SERVICE_CACHE, selectArticleViewsRecordServiceCacheConfig)
}.onFailure {
log.error(it) { "Failed to create cache" }
}
Expand Down
12 changes: 12 additions & 0 deletions api/src/main/kotlin/com/few/api/domain/article/repo/ArticleDao.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.few.api.domain.article.repo

import com.few.api.config.ApiLocalCacheConfig.Companion.ARTICLE_CONTENT_CACHE
import com.few.api.config.ApiLocalCacheConfig.Companion.LOCAL_CM
import com.few.api.config.ApiLocalCacheConfig.Companion.SELECT_ARTICLE_RECORD_CACHE
import com.few.api.domain.article.repo.command.InsertFullArticleRecordCommand
Expand Down Expand Up @@ -127,6 +128,17 @@ class ArticleDao(
.and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.eq(query.day))
.query

@Cacheable(key = "#articleId", cacheManager = LOCAL_CM, cacheNames = [ARTICLE_CONTENT_CACHE])
fun selectArticleContent(articleId: Long) =
dslContext
.select(
ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.`as`(SelectArticleContentsRecord::articleId.name),
ArticleIfo.ARTICLE_IFO.CONTENT.`as`(SelectArticleContentsRecord::content.name),
).from(ArticleIfo.ARTICLE_IFO)
.where(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.eq(articleId))
.and(ArticleIfo.ARTICLE_IFO.DELETED_AT.isNull)
.fetchOneInto(SelectArticleContentsRecord::class.java)

fun selectArticleContents(articleIds: Set<Long>): List<SelectArticleContentsRecord> =
selectArticleContentsQuery(articleIds)
.fetchInto(SelectArticleContentsRecord::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.few.api.domain.article.repo

import com.few.api.config.ApiLocalCacheConfig.Companion.ARTICLE_MAIN_CARD_RECORD_CACHE
import com.few.api.config.ApiLocalCacheConfig.Companion.LOCAL_CM
import com.few.api.domain.article.repo.command.ArticleMainCardExcludeWorkbookCommand
import com.few.api.domain.article.repo.command.UpdateArticleMainCardWorkbookCommand
import com.few.api.domain.article.repo.record.ArticleMainCardRecord
Expand All @@ -8,6 +10,7 @@ import com.few.api.domain.article.repo.support.CommonJsonMapper
import jooq.jooq_dsl.tables.ArticleMainCard.ARTICLE_MAIN_CARD
import org.jooq.*
import org.jooq.impl.DSL.*
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Repository

@Repository
Expand All @@ -16,6 +19,35 @@ class ArticleMainCardDao(
private val commonJsonMapper: CommonJsonMapper,
private val articleMainCardMapper: ArticleMainCardMapper,
) {
@Cacheable(key = "#articleId", cacheManager = LOCAL_CM, cacheNames = [ARTICLE_MAIN_CARD_RECORD_CACHE])
fun selectArticleMainCardsRecord(articleId: Long): ArticleMainCardRecord? =
dslContext
.select(
ARTICLE_MAIN_CARD.ID.`as`(ArticleMainCardRecord::articleId.name),
ARTICLE_MAIN_CARD.TITLE.`as`(ArticleMainCardRecord::articleTitle.name),
ARTICLE_MAIN_CARD.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name),
ARTICLE_MAIN_CARD.CATEGORY_CD.`as`(ArticleMainCardRecord::categoryCd.name),
ARTICLE_MAIN_CARD.CREATED_AT.`as`(ArticleMainCardRecord::createdAt.name),
ARTICLE_MAIN_CARD.WRITER_ID.`as`(ArticleMainCardRecord::writerId.name),
ARTICLE_MAIN_CARD.WRITER_EMAIL.`as`(ArticleMainCardRecord::writerEmail.name),
jsonGetAttributeAsText(
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION,
"name",
).`as`(ArticleMainCardRecord::writerName.name),
jsonGetAttribute(
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION,
"url",
).`as`(ArticleMainCardRecord::writerUrl.name),
jsonGetAttribute(
ARTICLE_MAIN_CARD.WRITER_DESCRIPTION,
"imageUrl",
).`as`(ArticleMainCardRecord::writerImgUrl.name),
ARTICLE_MAIN_CARD.WORKBOOKS.`as`(ArticleMainCardRecord::workbooks.name),
).from(ARTICLE_MAIN_CARD)
.where(ARTICLE_MAIN_CARD.ID.eq(articleId))
.fetch(articleMainCardMapper)
.firstOrNull()

fun selectArticleMainCardsRecord(articleIds: Set<Long>): Set<ArticleMainCardRecord> =
selectArticleMainCardsRecordQuery(articleIds)
.fetch(articleMainCardMapper)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.few.api.domain.article.usecase

import com.few.api.config.ApiLocalCacheConfig.Companion.LOCAL_CM
import com.few.api.config.ApiLocalCacheConfig.Companion.SELECT_ARTICLE_VIEWS_RECORD_SERVICE_CACHE
import com.few.api.domain.article.repo.ArticleViewCountDao
import com.few.api.domain.article.repo.query.SelectArticlesOrderByViewsQuery
import com.few.api.domain.article.repo.query.SelectRankByViewsQuery
import com.few.api.domain.article.repo.record.SelectArticleViewsRecord
import com.few.api.domain.common.vo.CategoryType
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service

@Service
class ArticleViewRecordsService(
private val articleViewCountDao: ArticleViewCountDao,
) {
@Cacheable(
keyGenerator = "articleViewRecordsServiceKeyGenerator",
cacheManager = LOCAL_CM,
cacheNames = [SELECT_ARTICLE_VIEWS_RECORD_SERVICE_CACHE],
)
fun execute(
preArticleId: Long,
categoryCd: Byte,
): MutableList<SelectArticleViewsRecord> {
/**
* 아티클 조회수 테이블에서 마지막 읽은 아티클 아이디, 카테고리를 기반으로 Offset(테이블 row 순위)을 구함
*/
val offset =
if (preArticleId <= 0) {
0L
} else {
articleViewCountDao.selectRankByViews(
SelectRankByViewsQuery(preArticleId),
) ?: 0
}

/**
* 구한 Offset을 기준으로 이번 스크롤에서 보여줄 아티클 11개를 뽑아옴
* 카테고리 별, 조회수 순 11개. 조회수가 같을 경우 최신 아티클이 우선순위를 가짐
*/
return articleViewCountDao
.selectArticlesOrderByViews(
SelectArticlesOrderByViewsQuery(
offset,
CategoryType.fromCode(categoryCd) ?: CategoryType.All,
),
).toMutableList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.few.api.domain.article.usecase

import org.springframework.cache.interceptor.KeyGenerator
import org.springframework.stereotype.Service
import java.lang.reflect.Method

@Service
class ArticleViewRecordsServiceKeyGenerator : KeyGenerator {
override fun generate(
target: Any,
method: Method,
vararg params: Any?,
): Any {
val preArticleId = params[0] as Long
val categoryCd = params[1] as Byte
return "$preArticleId ::$categoryCd"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,63 @@ package com.few.api.domain.article.usecase
import com.few.api.config.jooq.ApiTransactional
import com.few.api.domain.article.repo.ArticleDao
import com.few.api.domain.article.repo.ArticleMainCardDao
import com.few.api.domain.article.repo.ArticleViewCountDao
import com.few.api.domain.article.repo.query.SelectArticlesOrderByViewsQuery
import com.few.api.domain.article.repo.query.SelectRankByViewsQuery
import com.few.api.domain.article.repo.record.ArticleMainCardRecord
import com.few.api.domain.article.repo.record.SelectArticleContentsRecord
import com.few.api.domain.article.repo.record.SelectArticleViewsRecord
import com.few.api.domain.article.usecase.dto.*
import com.few.api.domain.common.exception.NotFoundException
import com.few.api.domain.common.vo.CategoryType
import org.springframework.stereotype.Component
import java.util.*
import kotlin.Comparator

@Component
class BrowseArticlesUseCase(
private val articleViewCountDao: ArticleViewCountDao,
private val articleViewRecordsService: ArticleViewRecordsService,
private val articleMainCardDao: ArticleMainCardDao,
private val articleDao: ArticleDao,
) {
@ApiTransactional(readOnly = true)
fun execute(useCaseIn: ReadArticlesUseCaseIn): ReadArticlesUseCaseOut {
/**
* 아티클 조회수 테이블에서 마지막 읽은 아티클 아이디, 카테고리를 기반으로 Offset(테이블 row 순위)을 구함
*/
val offset =
if (useCaseIn.prevArticleId <= 0) {
0L
} else {
articleViewCountDao.selectRankByViews(
SelectRankByViewsQuery(useCaseIn.prevArticleId),
) ?: 0
}

/**
* 구한 Offset을 기준으로 이번 스크롤에서 보여줄 아티클 11개를 뽑아옴
* 카테고리 별, 조회수 순 11개. 조회수가 같을 경우 최신 아티클이 우선순위를 가짐
*/
val articleViewsRecords: MutableList<SelectArticleViewsRecord> =
articleViewCountDao
.selectArticlesOrderByViews(
SelectArticlesOrderByViewsQuery(
offset,
CategoryType.fromCode(useCaseIn.categoryCd) ?: CategoryType.All,
),
).toMutableList()
val articleViewsRecords = articleViewRecordsService.execute(useCaseIn.prevArticleId, useCaseIn.categoryCd)

/**
* 11개를 조회한 상황에서 11개가 조회되지 않았다면 마지막 스크롤로 판단
*/
val isLast =
if (articleViewsRecords.size == 11) {
articleViewsRecords.removeAt(10)
false
} else {
true
when (articleViewsRecords.size) {
11 -> {
articleViewsRecords.removeAt(10)
false
}
10 -> {
false
}
else -> {
true
}
}
val articleIds = articleViewsRecords.map { it.articleId }.toList()

/**
* ARTICLE_MAIN_CARD 테이블에서 이번 스크롤에서 보여줄 10개 아티클 조회 (TODO: 캐싱 적용)
* ARTICLE_MAIN_CARD 테이블에서 이번 스크롤에서 보여줄 10개 아티클 조회
*/
val articleMainCardRecords: Set<ArticleMainCardRecord> =
articleMainCardDao.selectArticleMainCardsRecord(articleViewsRecords.map { it.articleId }.toSet())
val articleMainCardRecords: List<ArticleMainCardRecord> =
articleIds.mapNotNull {
articleMainCardDao.selectArticleMainCardsRecord(
it,
)
}

/**
* 아티클 컨텐츠는 ARTICLE_MAIN_CARD가 아닌 ARTICLE_IFO에서 조회 (TODO: 캐싱 적용)
*/
val selectArticleContentsRecords: List<SelectArticleContentsRecord> =
articleDao.selectArticleContents(articleMainCardRecords.map { it.articleId }.toSet())
setContentsToRecords(selectArticleContentsRecords, articleMainCardRecords)
val selectArticleContentsRecords: List<SelectArticleContentsRecord> = articleIds.mapNotNull { articleDao.selectArticleContent(it) }

/**
* 아티클 조회수 순, 조회수가 같을 경우 최신 아티클이 우선순위를 가지도록 정렬 (TODO: 삭제시 양향도 파악 필요)
*/
val sortedArticles = updateAndSortArticleViews(articleMainCardRecords, articleViewsRecords)
articleMainCardRecords.withIndex().forEach { (index, articleMainCardRecord) ->
articleMainCardRecord.content = selectArticleContentsRecords[index].content
}

val articleUseCaseOuts: List<ReadArticleUseCaseOut> =
sortedArticles
articleMainCardRecords
.map { a ->
ReadArticleUseCaseOut(
id = a.articleId,
Expand Down Expand Up @@ -107,52 +87,4 @@ class BrowseArticlesUseCase(

return ReadArticlesUseCaseOut(articleUseCaseOuts, isLast)
}

private fun updateAndSortArticleViews(
articleRecords: Set<ArticleMainCardRecord>,
articleViewsRecords: List<SelectArticleViewsRecord>,
): Set<ArticleMainCardRecord> {
val sortedSet =
TreeSet(
Comparator<ArticleMainCardRecord> { a1, a2 ->
// views 값이 null일 경우 0으로 간주
val views1 = a1.views ?: 0
val views2 = a2.views ?: 0

// views 내림차순 정렬
val viewComparison = views2.compareTo(views1)

if (viewComparison != 0) {
viewComparison
} else {
// views가 같을 경우 articleId 내림차순 정렬(최신글)
val articleId1 = a1.articleId
val articleId2 = a2.articleId
articleId2.compareTo(articleId1)
}
},
)

val viewsMap = articleViewsRecords.associateBy({ it.articleId }, { it.views })

articleRecords.forEach { article ->
val updatedViews = viewsMap[article.articleId] ?: 0
article.views = updatedViews
sortedSet.add(article)
}

return sortedSet
}

private fun setContentsToRecords(
articleContentsRecords: List<SelectArticleContentsRecord>,
articleMainCardRecords: Set<ArticleMainCardRecord>,
) {
val articleMainCardRecordsMap: Map<Long, ArticleMainCardRecord> =
articleMainCardRecords.associateBy { it.articleId }

articleContentsRecords.map { articleContentRecord ->
articleMainCardRecordsMap[articleContentRecord.articleId]?.content = articleContentRecord.content
}
}
}

0 comments on commit 59eb065

Please sign in to comment.