Skip to content

Commit

Permalink
[Feat/#221] 아티클 조회수 저장 방식 및 조회 방식 개선 (#223)
Browse files Browse the repository at this point in the history
* feat: ARTICLE_VIEW_COUNT 테이블 생성, 아티클 조회수 순 정렬 등에 사용 예정

* fix: ARTICLE_VIEW_COUNT 테이블 deleted_at 추가

* feat: 아티클 조회시 article view his가 아닌 article view count를 보도록 수정(SL수정: count -> select)

* test: ReadArticleUseCaseTest 수정 반영

* feat: ArticleViewCountHandler 추가

* fix: view_count 컬럼 기준 내림차순 정렬 인덱스로 변경

* feat: category_cd 컬럼 추가 in ARTICLE_VIEW_COUNT 테이블 and 인덱싱

* test: category_cd 컬럼 추가 테스트 반영

* refactor: article category 유효하지 않을 경우에 대한 검증 추가

* feat: article.invalid.category 에러코드 추가

---------

Co-authored-by: belljun3395 <[email protected]>
  • Loading branch information
hun-ca and belljun3395 authored Jul 21, 2024
1 parent 656db6d commit 28d00e8
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.few.api.repo.dao.article

import com.few.api.repo.dao.article.command.ArticleViewCountCommand
import com.few.api.repo.dao.article.query.ArticleViewCountQuery
import jooq.jooq_dsl.Tables.ARTICLE_VIEW_COUNT
import org.jooq.DSLContext
import org.springframework.stereotype.Repository

@Repository
class ArticleViewCountDao(
private val dslContext: DSLContext,
) {

fun upsertArticleViewCount(query: ArticleViewCountQuery) {
dslContext.insertInto(ARTICLE_VIEW_COUNT)
.set(ARTICLE_VIEW_COUNT.ARTICLE_ID, query.articleId)
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, 1)
.set(ARTICLE_VIEW_COUNT.CATEGORY_CD, query.categoryType.code)
.onDuplicateKeyUpdate()
.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, ARTICLE_VIEW_COUNT.VIEW_COUNT.plus(1))
}

fun selectArticleViewCount(command: ArticleViewCountCommand): Long? {
return dslContext.select(
ARTICLE_VIEW_COUNT.VIEW_COUNT
).from(ARTICLE_VIEW_COUNT)
.where(ARTICLE_VIEW_COUNT.ARTICLE_ID.eq(command.articleId))
.and(ARTICLE_VIEW_COUNT.DELETED_AT.isNull)
.fetchOneInto(Long::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.article.command

data class ArticleViewCountCommand(
val articleId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.few.api.repo.dao.article.query

import com.few.data.common.code.CategoryType

data class ArticleViewCountQuery(
val articleId: Long,
val categoryType: CategoryType,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.few.api.domain.article.handler

import com.few.api.repo.dao.article.ArticleViewCountDao
import com.few.api.repo.dao.article.command.ArticleViewCountCommand
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

@Component
class ArticleViewCountHandler(
private val articleViewCountDao: ArticleViewCountDao,
) {
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW)
fun browseArticleViewCount(articleId: Long): Long {
return (articleViewCountDao.selectArticleViewCount(ArticleViewCountCommand(articleId)) ?: 0L) + 1L
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.few.api.domain.article.handler

import com.few.api.config.DatabaseAccessThreadPoolConfig.Companion.DATABASE_ACCESS_POOL
import com.few.api.repo.dao.article.ArticleViewCountDao
import com.few.api.repo.dao.article.ArticleViewHisDao
import com.few.api.repo.dao.article.command.ArticleViewHisCommand
import com.few.api.repo.dao.article.query.ArticleViewCountQuery
import com.few.data.common.code.CategoryType
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
Expand All @@ -11,17 +14,24 @@ import org.springframework.transaction.annotation.Transactional
@Component
class ArticleViewHisAsyncHandler(
private val articleViewHisDao: ArticleViewHisDao,
private val articleViewCountDao: ArticleViewCountDao,
) {
private val log = KotlinLogging.logger {}

@Async(value = DATABASE_ACCESS_POOL)
@Transactional
fun addArticleViewHis(articleId: Long, memberId: Long) {
fun addArticleViewHis(articleId: Long, memberId: Long, categoryType: CategoryType) {
try {
articleViewHisDao.insertArticleViewHis(ArticleViewHisCommand(articleId, memberId))
log.debug { "Successfully inserted article view history for articleId: $articleId and memberId: $memberId" }

articleViewCountDao.upsertArticleViewCount(ArticleViewCountQuery(articleId, categoryType))
log.debug { "Successfully upserted article view count for articleId: $articleId and memberId: $memberId" }
} catch (e: Exception) {
log.error { "Failed to insert article view history for articleId: $articleId and memberId: $memberId" }
log.error {
"Failed insertion article view history and upsertion article view count " +
"for articleId: $articleId and memberId: $memberId"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.few.api.domain.article.usecase

import com.few.api.domain.article.handler.ArticleViewCountHandler
import com.few.api.domain.article.handler.ArticleViewHisAsyncHandler
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseOut
Expand All @@ -10,8 +11,6 @@ import com.few.api.domain.article.service.dto.BrowseArticleProblemIdsInDto
import com.few.api.domain.article.service.dto.ReadWriterRecordInDto
import com.few.api.exception.common.NotFoundException
import com.few.api.repo.dao.article.ArticleDao
import com.few.api.repo.dao.article.ArticleViewHisDao
import com.few.api.repo.dao.article.query.ArticleViewHisCountQuery
import com.few.api.repo.dao.article.query.SelectArticleRecordQuery
import com.few.data.common.code.CategoryType
import org.springframework.stereotype.Component
Expand All @@ -22,8 +21,8 @@ class ReadArticleUseCase(
private val articleDao: ArticleDao,
private val readArticleWriterRecordService: ReadArticleWriterRecordService,
private val browseArticleProblemsService: BrowseArticleProblemsService,
private val articleViewHisDao: ArticleViewHisDao,
private val articleViewHisAsyncHandler: ArticleViewHisAsyncHandler,
private val articleViewCountHandler: ArticleViewCountHandler,
) {

@Transactional(readOnly = true)
Expand All @@ -41,9 +40,13 @@ class ReadArticleUseCase(
browseArticleProblemsService.execute(query)
}

val views = (articleViewHisDao.countArticleViews(ArticleViewHisCountQuery(useCaseIn.articleId)) ?: 0L) + 1L

articleViewHisAsyncHandler.addArticleViewHis(useCaseIn.articleId, useCaseIn.memberId)
// ARTICLE VIEW HIS에 저장하기 전에 먼저 VIEW COUNT 조회하는 순서 변경 금지
val views = articleViewCountHandler.browseArticleViewCount(useCaseIn.articleId)
articleViewHisAsyncHandler.addArticleViewHis(
useCaseIn.articleId,
useCaseIn.memberId,
CategoryType.fromCode(articleRecord.category) ?: throw NotFoundException("article.invalid.category")
)

return ReadArticleUseCaseOut(
id = articleRecord.articleId,
Expand Down
1 change: 1 addition & 0 deletions api/src/main/resources/messages/article.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
article.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0069\u0064
article.notfound.articleidworkbookid=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0069\u0064\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u0069
article.invalid.category=\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\ub294\u0020\uc544\ud2f0\ud074\u0020\uce74\ud14c\uace0\ub9ac\uc785\ub2c8\ub2e4\u000d
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.few.api.domain.article.usecase

import com.few.api.domain.article.handler.ArticleViewCountHandler
import com.few.api.domain.article.handler.ArticleViewHisAsyncHandler
import com.few.api.domain.article.service.BrowseArticleProblemsService
import com.few.api.domain.article.service.ReadArticleWriterRecordService
import com.few.api.domain.article.service.dto.BrowseArticleProblemsOutDto
import com.few.api.domain.article.service.dto.ReadWriterOutDto
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn
import com.few.api.repo.dao.article.ArticleDao
import com.few.api.repo.dao.article.ArticleViewHisDao
import com.few.api.repo.dao.article.record.SelectArticleRecord
import io.github.oshai.kotlinlogging.KotlinLogging
import io.kotest.assertions.throwables.shouldThrow
Expand All @@ -24,22 +24,22 @@ class ReadArticleUseCaseTest : BehaviorSpec({
lateinit var readArticleWriterRecordService: ReadArticleWriterRecordService
lateinit var browseArticleProblemsService: BrowseArticleProblemsService
lateinit var useCase: ReadArticleUseCase
lateinit var articleViewHisDao: ArticleViewHisDao
lateinit var articleViewHisAsyncHandler: ArticleViewHisAsyncHandler
lateinit var articleViewCountHandler: ArticleViewCountHandler
val useCaseIn = ReadArticleUseCaseIn(articleId = 1L, memberId = 1L)

beforeContainer {
articleDao = mockk<ArticleDao>()
readArticleWriterRecordService = mockk<ReadArticleWriterRecordService>()
browseArticleProblemsService = mockk<BrowseArticleProblemsService>()
articleViewHisDao = mockk<ArticleViewHisDao>()
articleViewHisAsyncHandler = mockk<ArticleViewHisAsyncHandler>()
articleViewCountHandler = mockk<ArticleViewCountHandler>()
useCase = ReadArticleUseCase(
articleDao,
readArticleWriterRecordService,
browseArticleProblemsService,
articleViewHisDao,
articleViewHisAsyncHandler
articleViewHisAsyncHandler,
articleViewCountHandler
)
}

Expand All @@ -64,8 +64,8 @@ class ReadArticleUseCaseTest : BehaviorSpec({
every { articleDao.selectArticleRecord(any()) } returns record
every { readArticleWriterRecordService.execute(any()) } returns writerSvcOutDto
every { browseArticleProblemsService.execute(any()) } returns probSvcOutDto
every { articleViewHisDao.countArticleViews(any()) } returns 1L
every { articleViewHisAsyncHandler.addArticleViewHis(any(), any()) } answers {
every { articleViewCountHandler.browseArticleViewCount(any()) } returns 1L
every { articleViewHisAsyncHandler.addArticleViewHis(any(), any(), any()) } answers {
log.debug { "Inserting article view history asynchronously" }
}

Expand All @@ -75,8 +75,8 @@ class ReadArticleUseCaseTest : BehaviorSpec({
verify(exactly = 1) { articleDao.selectArticleRecord(any()) }
verify(exactly = 1) { readArticleWriterRecordService.execute(any()) }
verify(exactly = 1) { browseArticleProblemsService.execute(any()) }
verify(exactly = 1) { articleViewHisDao.countArticleViews(any()) }
verify(exactly = 1) { articleViewHisAsyncHandler.addArticleViewHis(any(), any()) }
verify(exactly = 1) { articleViewCountHandler.browseArticleViewCount(any()) }
verify(exactly = 1) { articleViewHisAsyncHandler.addArticleViewHis(any(), any(), any()) }
}
}

Expand Down
16 changes: 16 additions & 0 deletions data/db/migration/entity/V1.00.0.14__manage_article_view_count.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- article 별 조회수 저장 테이블
CREATE TABLE ARTICLE_VIEW_COUNT
(
article_id BIGINT NOT NULL,
view_count BIGINT NOT NULL,
category_cd TINYINT NOT NULL,
deleted_at TIMESTAMP NULL DEFAULT NULL,
CONSTRAINT article_view_count_pk PRIMARY KEY (article_id)
);

-- 조회수 순으로 아티클 조회시 사용하기 위한 인덱스
-- ex. SELECT * FROM ARTICLE_VIEW_COUNT ORDER BY view_count DESC LIMIT 10;
CREATE INDEX article_view_count_idx1 ON ARTICLE_VIEW_COUNT (view_count DESC);

-- 카테고리 별 필터링을 위한 인덱스
CREATE INDEX article_view_count_idx2 ON ARTICLE_VIEW_COUNT (category_cd);

0 comments on commit 28d00e8

Please sign in to comment.