From f1717c0142047b2399ac18a45da5791bbbd2e06c Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 11 Jan 2025 15:40:47 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EC=BD=94=EB=A3=A8=ED=8B=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/build.gradle.kts | 1 + .../few/api/domain/article/repo/ArticleDao.kt | 13 ++ .../domain/article/repo/ArticleMainCardDao.kt | 27 ++- .../article/repo/ArticleViewCountDao.kt | 19 +- .../article/usecase/BrowseArticlesUseCase.kt | 179 +++++++++--------- buildSrc/src/main/kotlin/DependencyVersion.kt | 2 +- 6 files changed, 144 insertions(+), 97 deletions(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index cf14558b5..3d8a67355 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { /** jooq */ jooqCodegen("org.jooq:jooq-meta-extensions:${DependencyVersion.JOOQ}") implementation("org.springframework.boot:spring-boot-starter-jooq") + implementation("org.jooq:jooq-kotlin-coroutines:${DependencyVersion.JOOQ}") /** flyway */ implementation("org.flywaydb:flyway-core:${DependencyVersion.FLYWAY}") diff --git a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleDao.kt b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleDao.kt index 60ac23d99..0d54d09f2 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleDao.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleDao.kt @@ -8,6 +8,8 @@ import com.few.api.domain.article.repo.record.* import com.few.api.domain.common.vo.MemberType import jooq.jooq_dsl.tables.* import jooq.jooq_dsl.tables.MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.awaitSingle import org.jooq.* import org.jooq.impl.DSL import org.springframework.cache.annotation.Cacheable @@ -131,6 +133,17 @@ class ArticleDao( selectArticleContentsQuery(articleIds) .fetchInto(SelectArticleContentsRecord::class.java) + suspend fun selectArticleContentsAsync(articleId: Long): SelectArticleContentsRecord = + 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) + .awaitSingle() + .into(SelectArticleContentsRecord::class.java) + fun selectArticleContentsQuery(articleIds: Set) = dslContext .select( diff --git a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleMainCardDao.kt b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleMainCardDao.kt index 52b696060..2335066e9 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleMainCardDao.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleMainCardDao.kt @@ -6,6 +6,7 @@ import com.few.api.domain.article.repo.record.ArticleMainCardRecord import com.few.api.domain.article.repo.support.ArticleMainCardMapper import com.few.api.domain.article.repo.support.CommonJsonMapper import jooq.jooq_dsl.tables.ArticleMainCard.ARTICLE_MAIN_CARD +import kotlinx.coroutines.reactive.awaitSingle import org.jooq.* import org.jooq.impl.DSL.* import org.springframework.stereotype.Repository @@ -18,9 +19,34 @@ class ArticleMainCardDao( ) { fun selectArticleMainCardsRecord(articleIds: Set): Set = selectArticleMainCardsRecordQuery(articleIds) + .query .fetch(articleMainCardMapper) .toSet() + suspend fun selectArticleMainCardsRecordAsync(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)) + .awaitSingle() + .map { + articleMainCardMapper.map(it) + } + fun selectArticleMainCardsRecordQuery(articleIds: Set) = dslContext .select( @@ -40,7 +66,6 @@ class ArticleMainCardDao( ARTICLE_MAIN_CARD.WORKBOOKS.`as`(ArticleMainCardRecord::workbooks.name), ).from(ARTICLE_MAIN_CARD) .where(ARTICLE_MAIN_CARD.ID.`in`(articleIds)) - .query /** * NOTE - The query performed in this function do not save the workbook. diff --git a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleViewCountDao.kt b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleViewCountDao.kt index d6c4b45cd..0e3855b34 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleViewCountDao.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/repo/ArticleViewCountDao.kt @@ -24,9 +24,12 @@ import com.few.api.domain.article.repo.record.SelectArticleViewsRecord import com.few.api.domain.common.vo.CategoryType import jooq.jooq_dsl.tables.ArticleViewCount.ARTICLE_VIEW_COUNT import jooq.jooq_dsl.tables.SendArticleEventHistory.SEND_ARTICLE_EVENT_HISTORY +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.awaitSingle import org.jooq.DSLContext import org.jooq.impl.DSL.* import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux object TempTable { const val ARTICLE_ID_COLUMN = "ARTICLE_ID" @@ -93,8 +96,14 @@ class ArticleViewCountDao( fun selectRankByViews(query: SelectRankByViewsQuery): Long? = selectRankByViewsQuery(query) + .query .fetchOneInto(Long::class.java) + suspend fun selectRankByViewsAsync(query: SelectRankByViewsQuery): Long? = + selectRankByViewsQuery(query) + .awaitSingle() + .into(Long::class.java) + fun selectRankByViewsQuery(query: SelectRankByViewsQuery) = dslContext .select(field(ROW_RANK_TABLE_OFFSET, Long::class.java)) @@ -135,12 +144,19 @@ class ArticleViewCountDao( ).asTable(TOTAL_VIEW_COUNT_TABLE), ).asTable(ROW_RANK_TABLE), ).where(field(ROW_RANK_TABLE_ARTICLE_ID).eq(query.articleId)) - .query fun selectArticlesOrderByViews(query: SelectArticlesOrderByViewsQuery): List = selectArticlesOrderByViewsQuery(query) + .query .fetchInto(SelectArticleViewsRecord::class.java) + suspend fun selectArticlesOrderByViewsAsync(query: SelectArticlesOrderByViewsQuery): List = + Flux + .from(selectArticlesOrderByViewsQuery(query).query) + .map { it.into(SelectArticleViewsRecord::class.java) } + .collectList() + .awaitSingle() + fun selectArticlesOrderByViewsQuery(query: SelectArticlesOrderByViewsQuery) = dslContext .select( @@ -186,5 +202,4 @@ class ArticleViewCountDao( else -> field(ARTICLE_VIEW_COUNT_OFFSET_TABLE_CATEGORY_CD).eq(query.category.code) }, ).limit(11) - .query } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt index 113664d31..145fe135a 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt @@ -7,11 +7,13 @@ 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import org.springframework.stereotype.Component import java.util.* import kotlin.Comparator @@ -24,88 +26,91 @@ class BrowseArticlesUseCase( ) { @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 = - articleViewCountDao - .selectArticlesOrderByViews( - SelectArticlesOrderByViewsQuery( - offset, - CategoryType.fromCode(useCaseIn.categoryCd) ?: CategoryType.All, - ), - ).toMutableList() - - /** - * 11개를 조회한 상황에서 11개가 조회되지 않았다면 마지막 스크롤로 판단 - */ - val isLast = - if (articleViewsRecords.size == 11) { - articleViewsRecords.removeAt(10) - false - } else { - true + return runBlocking(Dispatchers.IO) { + /** + * 아티클 조회수 테이블에서 마지막 읽은 아티클 아이디, 카테고리를 기반으로 Offset(테이블 row 순위)을 구함 + */ + val offset = + if (useCaseIn.prevArticleId <= 0) { + 0L + } else { + articleViewCountDao.selectRankByViewsAsync( + SelectRankByViewsQuery(useCaseIn.prevArticleId), + ) ?: 0L + } + + /** + * 구한 Offset을 기준으로 이번 스크롤에서 보여줄 아티클 11개를 뽑아옴 + * 카테고리 별, 조회수 순 11개. 조회수가 같을 경우 최신 아티클이 우선순위를 가짐 + */ + val articleViewsRecords: MutableList = + articleViewCountDao + .selectArticlesOrderByViewsAsync( + SelectArticlesOrderByViewsQuery( + offset, + CategoryType.fromCode(useCaseIn.categoryCd) ?: CategoryType.All, + ), + ).toMutableList() + + /** + * 11개를 조회한 상황에서 11개가 조회되지 않았다면 마지막 스크롤로 판단 + */ + val isLast = + if (articleViewsRecords.size == 11) { + articleViewsRecords.removeAt(10) + false + } else { + true + } + + val recordViewIds = articleViewsRecords.map { it.articleId }.toSet() + val articleMainCardRecords = mutableSetOf() + recordViewIds.forEach { id -> + async { + val articleMainCardRecord = articleMainCardDao.selectArticleMainCardsRecordAsync(id) + val selectArticleContentsRecord = articleDao.selectArticleContentsAsync(id) + + articleMainCardRecord?.apply { + this.content = selectArticleContentsRecord.content + articleMainCardRecords.add(this) + } + }.await() } - /** - * ARTICLE_MAIN_CARD 테이블에서 이번 스크롤에서 보여줄 10개 아티클 조회 (TODO: 캐싱 적용) - */ - val articleMainCardRecords: Set = - articleMainCardDao.selectArticleMainCardsRecord(articleViewsRecords.map { it.articleId }.toSet()) - - /** - * 아티클 컨텐츠는 ARTICLE_MAIN_CARD가 아닌 ARTICLE_IFO에서 조회 (TODO: 캐싱 적용) - */ - val selectArticleContentsRecords: List = - articleDao.selectArticleContents(articleMainCardRecords.map { it.articleId }.toSet()) - setContentsToRecords(selectArticleContentsRecords, articleMainCardRecords) - - /** - * 아티클 조회수 순, 조회수가 같을 경우 최신 아티클이 우선순위를 가지도록 정렬 (TODO: 삭제시 양향도 파악 필요) - */ - val sortedArticles = updateAndSortArticleViews(articleMainCardRecords, articleViewsRecords) - - val articleUseCaseOuts: List = - sortedArticles - .map { a -> - ReadArticleUseCaseOut( - id = a.articleId, - writer = - WriterDetail( - id = a.writerId, - name = a.writerName, - imageUrl = a.writerImgUrl, - url = a.writerUrl, - ), - mainImageUrl = a.mainImageUrl, - title = a.articleTitle, - content = a.content, - problemIds = emptyList(), - category = - CategoryType.fromCode(a.categoryCd)?.displayName - ?: throw NotFoundException("article.invalid.category"), - createdAt = a.createdAt, - views = a.views, - workbooks = - a.workbooks - .map { WorkbookDetail(it.id!!, it.title!!) }, - ) - }.toList() - - return ReadArticlesUseCaseOut(articleUseCaseOuts, isLast) + /** + * 아티클 조회수 순, 조회수가 같을 경우 최신 아티클이 우선순위를 가지도록 정렬 (TODO: 삭제시 양향도 파악 필요) + */ + val sortedArticles = updateAndSortArticleViews(articleMainCardRecords, articleViewsRecords) + + val articleUseCaseOuts: List = + sortedArticles + .map { a -> + ReadArticleUseCaseOut( + id = a.articleId, + writer = + WriterDetail( + id = a.writerId, + name = a.writerName, + imageUrl = a.writerImgUrl, + url = a.writerUrl, + ), + mainImageUrl = a.mainImageUrl, + title = a.articleTitle, + content = a.content, + problemIds = emptyList(), + category = + CategoryType.fromCode(a.categoryCd)?.displayName + ?: throw NotFoundException("article.invalid.category"), + createdAt = a.createdAt, + views = a.views, + workbooks = + a.workbooks + .map { WorkbookDetail(it.id!!, it.title!!) }, + ) + }.toList() + + return@runBlocking ReadArticlesUseCaseOut(articleUseCaseOuts, isLast) + } } private fun updateAndSortArticleViews( @@ -143,16 +148,4 @@ class BrowseArticlesUseCase( return sortedSet } - - private fun setContentsToRecords( - articleContentsRecords: List, - articleMainCardRecords: Set, - ) { - val articleMainCardRecordsMap: Map = - articleMainCardRecords.associateBy { it.articleId } - - articleContentsRecords.map { articleContentRecord -> - articleMainCardRecordsMap[articleContentRecord.articleId]?.content = articleContentRecord.content - } - } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/DependencyVersion.kt b/buildSrc/src/main/kotlin/DependencyVersion.kt index 4bfc08d7e..92e8fce23 100644 --- a/buildSrc/src/main/kotlin/DependencyVersion.kt +++ b/buildSrc/src/main/kotlin/DependencyVersion.kt @@ -28,7 +28,7 @@ object DependencyVersion { const val FLYWAY = "9.16.0" /** jooq */ - const val JOOQ = "3.19.10" + const val JOOQ = "3.19.17" /** test */ const val MOCKK = "1.13.9"