Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/#221] 아티클 조회수 저장 방식 및 조회 방식 개선 #223

Merged
merged 13 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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)
.onDuplicateKeyUpdate()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거는 뭔가요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

article_view_count 테이블 로우는 아티클 당 한개로 구성되는데, 첫번째로 조회될 땐 insert가 가고, 이미 row가 있는 경우(이미 1번 이상 조회됨) Update(view_count + 1) 쿼리가 날라가는 upsert 쿼리입니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 신기하다..!!!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQL 변환

INSERT INTO ARTICLE_VIEW_COUNT (ARTICLE_ID, VIEW_COUNT, CATEGORY_CD)
VALUES ($articleId, 1, $category)
ON DUPLICATE KEY UPDATE
VIEW_COUNT = VIEW_COUNT + 1;

.set(ARTICLE_VIEW_COUNT.VIEW_COUNT, ARTICLE_VIEW_COUNT.VIEW_COUNT.plus(1))
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARTICLE_VIEW_COUNT 테이블에 조회수 컬럼을 upsert함.
기존 로우가 존재하면 count 값만 늘리고 로우 자체가 없으면 Insert진행 (최초 아티클 조회시에 해당)


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,5 @@
package com.few.api.repo.dao.article.query

data class ArticleViewCountQuery(
val articleId: Long,
)
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 @ViewTransactional 요런 어노테이션으로 선언 해버릴까요?

fun browseArticleViewCount(articleId: Long): Long {
return (articleViewCountDao.selectArticleViewCount(ArticleViewCountCommand(articleId)) ?: 0L) + 1L
}
}
Comment on lines +10 to +18
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아티클 조회수를 조회하는 서비스

  • Isolation.READ_UNCOMMITTED: 조회수 데이터 정합성 필요없이 최대한 빠르게 조회하기 위해 격리 레벨을 최하위로 낮춤. 조회수 자체가 크게 중요한 데이터도 아니고 해서 커밋 안되었더라도 읽게 함
  • Propagation.REQUIRES_NEW: 기존 트랜잭션이 있을 경우 새로운 트랜잭션을 만들어서 진행. (기존 트랜잭션에 격리레벨을 분리하기 위함)

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
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 io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
Expand All @@ -11,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional
@Component
class ArticleViewHisAsyncHandler(
private val articleViewHisDao: ArticleViewHisDao,
private val articleViewCountDao: ArticleViewCountDao,
) {
private val log = KotlinLogging.logger {}

Expand All @@ -20,8 +23,14 @@ class ArticleViewHisAsyncHandler(
try {
articleViewHisDao.insertArticleViewHis(ArticleViewHisCommand(articleId, memberId))
log.debug { "Successfully inserted article view history for articleId: $articleId and memberId: $memberId" }

articleViewCountDao.upsertArticleViewCount(ArticleViewCountQuery(articleId))
log.debug { "Successfully upserted article view count for articleId: $articleId and memberId: $memberId" }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에 아티클 view his 테이블에 저장하는걸 async로 구성했는데, 마찬가지로 해당 과정에서 Insert가 끝나면 ARTICLE_VIEW_COUNT 테이블에 Upsert를 진행함. (어제 DB 트리거로 만들어뒀던 부분을 애플리케이션 단으로 옮긴 것)

} 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,8 +40,8 @@ class ReadArticleUseCase(
browseArticleProblemsService.execute(query)
}

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

// ARTICLE VIEW HIS에 저장하기 전에 먼저 VIEW COUNT 조회하는 순서 변경 금지
val views = articleViewCountHandler.browseArticleViewCount(useCaseIn.articleId)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요친구는 왜 네이밍이 handler이죠? 저는 반환을 하니 service가 어울릴꺼 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

service도 생각했었는데 같은 도메인이여서 네이밍 뭘로할지 고민되네요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

진짜 애매하다..

articleViewHisAsyncHandler.addArticleViewHis(useCaseIn.articleId, useCaseIn.memberId)

return ReadArticleUseCaseOut(
Expand Down
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,7 +64,7 @@ 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 { articleViewCountHandler.browseArticleViewCount(any()) } returns 1L
every { articleViewHisAsyncHandler.addArticleViewHis(any(), any()) } answers {
log.debug { "Inserting article view history asynchronously" }
}
Expand All @@ -75,7 +75,7 @@ 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) { articleViewCountHandler.browseArticleViewCount(any()) }
verify(exactly = 1) { articleViewHisAsyncHandler.addArticleViewHis(any(), any()) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- article 별 조회수 저장 테이블
CREATE TABLE ARTICLE_VIEW_COUNT
(
article_id BIGINT NOT NULL,
view_count BIGINT NOT NULL,
deleted_at TIMESTAMP NULL DEFAULT NULL,
CONSTRAINT article_view_count_pk PRIMARY KEY (article_id)
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARTICLE_VIEW_COUNT 테이블 정의

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(참고로 PK 이름 지정함: article_view_count_pk)


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