Skip to content

Commit

Permalink
[Feat/#215] article_view_his 테이블 추가 및 조회수 응답 추가 (#216)
Browse files Browse the repository at this point in the history
* feat: flyway V1.00.0.7 배포(ARTICLE_VIEW_HIS 테이블 추가)

* feat: article view history 저장 로직 추가

* feat: article 조회시 views 필드 추가

* test: article 조회수 추가 관련 test 코드 수정

* feat: ARTICLE_VIEW_HIS 테이블 인덱스 추가(article_mst_id 컬럼)

* fix: 인덱스 테이블 명 수정

* refactor: DAO 메소드 명 수정

* refactor: 아티클 뷰와 아티클 도메인 통합

* refactor: DAO 빈 지정 stereotype 변경(@component -> @repository)

* refactor: 조회수 결정에 대한 로직을 DAO -> UC로 이동

* chore: member Id 사용에 대한 TODO 주석 추가

* feat: 아티클 조회 기록 Insert과정을 다른 쓰레드에서 수행하도록 변경

* fix: add ArticleViewHisAsyncEvent

* test: ReadArticleUseCaseTest 수정반영

* refactor: SubscribeWorkbookUseCaseTest 에서 print 삭제 및 log 적용

* refactor: rename ArticleViewHisAsyncHandler

* test: application-test.yaml에 database async thread pool 설정 추가

* fix: 테스트 통과하지 못하는 문제 해결

* feat: 아티클 조회응답 바디 views 필드 추가

---------

Co-authored-by: belljun3395 <[email protected]>
  • Loading branch information
hun-ca and belljun3395 authored Jul 19, 2024
1 parent 4c8c4ce commit 7ffe45c
Show file tree
Hide file tree
Showing 27 changed files with 213 additions and 35 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,10 @@ Footer
*.log
*.tmp

# DB schema migration path (except data module)
data/src/main/resources/**/*.sql
api/**/*.sql
api-repo/**/*.sql
batch/**/*.sql
email/**/*.sql
storage/**/*.sql
2 changes: 0 additions & 2 deletions api-repo/.gitignore

This file was deleted.

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.ArticleViewHisCommand
import com.few.api.repo.dao.article.query.ArticleViewHisCountQuery
import jooq.jooq_dsl.tables.ArticleViewHis
import org.jooq.DSLContext
import org.springframework.stereotype.Repository

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

fun insertArticleViewHis(command: ArticleViewHisCommand) {
dslContext.insertInto(
ArticleViewHis.ARTICLE_VIEW_HIS,
ArticleViewHis.ARTICLE_VIEW_HIS.ARTICLE_MST_ID,
ArticleViewHis.ARTICLE_VIEW_HIS.MEMBER_ID
).values(
command.articleId,
command.memberId
).execute()
}

fun countArticleViews(query: ArticleViewHisCountQuery): Long? {
return dslContext.selectCount()
.from(ArticleViewHis.ARTICLE_VIEW_HIS)
.where(ArticleViewHis.ARTICLE_VIEW_HIS.ARTICLE_MST_ID.eq(query.articleId))
.fetchOne(0, Long::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.article.command

data class ArticleViewHisCommand(
val articleId: Long,
val memberId: 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 ArticleViewHisCountQuery(
val articleId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import jooq.jooq_dsl.tables.Problem
import org.jooq.DSLContext
import org.jooq.JSON
import org.jooq.impl.DSL
import org.springframework.stereotype.Component
import org.springframework.stereotype.Repository

@Component
@Repository
class ProblemDao(
private val dslContext: DSLContext,
private val contentsJsonMapper: ContentsJsonMapper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.few.api.repo.dao.problem
import com.few.api.repo.dao.problem.command.InsertSubmitHistoryCommand
import jooq.jooq_dsl.Tables.SUBMIT_HISTORY
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import org.springframework.stereotype.Repository

@Component
@Repository
class SubmitHistoryDao(
private val dslContext: DSLContext,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import com.few.api.repo.dao.subscription.record.CountAllSubscriptionStatusRecord
import jooq.jooq_dsl.Tables.MAPPING_WORKBOOK_ARTICLE
import jooq.jooq_dsl.Tables.SUBSCRIPTION
import org.jooq.DSLContext
import org.springframework.stereotype.Component
import org.springframework.stereotype.Repository
import java.time.LocalDateTime

@Component
@Repository
class SubscriptionDao(
private val dslContext: DSLContext,
) {
Expand Down
2 changes: 0 additions & 2 deletions api/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.few.api.config

import com.few.api.config.properties.ThreadPoolProperties
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor

@Configuration
class DatabaseAccessThreadPoolConfig {
private val log = KotlinLogging.logger {}

companion object {
const val DATABASE_ACCESS_POOL = "database-task-"
}

@Bean
@ConfigurationProperties(prefix = "database.thread-pool")
fun databaseAccessThreadPoolProperties(): ThreadPoolProperties {
return ThreadPoolProperties()
}

@Bean(DATABASE_ACCESS_POOL)
fun databaseAccessThreadPool() = ThreadPoolTaskExecutor().apply {
val properties = databaseAccessThreadPoolProperties()
corePoolSize = properties.getCorePoolSize()
maxPoolSize = properties.getMaxPoolSize()
queueCapacity = properties.getQueueCapacity()
setWaitForTasksToCompleteOnShutdown(properties.getWaitForTasksToCompleteOnShutdown())
setAwaitTerminationSeconds(properties.getAwaitTerminationSeconds())
setThreadNamePrefix("databaseAccessThreadPool-")
setRejectedExecutionHandler { r, _ ->
log.warn { "Database Access Task Rejected: $r" }
}
initialize()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.few.api.domain.article.handler

import com.few.api.config.DatabaseAccessThreadPoolConfig.Companion.DATABASE_ACCESS_POOL
import com.few.api.repo.dao.article.ArticleViewHisDao
import com.few.api.repo.dao.article.command.ArticleViewHisCommand
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class ArticleViewHisAsyncHandler(
private val articleViewHisDao: ArticleViewHisDao,
) {
private val log = KotlinLogging.logger {}

@Async(value = DATABASE_ACCESS_POOL)
@Transactional
fun addArticleViewHis(articleId: Long, memberId: Long) {
try {
articleViewHisDao.insertArticleViewHis(ArticleViewHisCommand(articleId, memberId))
log.debug { "Successfully inserted article view history for articleId: $articleId and memberId: $memberId" }
} catch (e: Exception) {
log.error { "Failed to insert article view history 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.ArticleViewHisAsyncHandler
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn
import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseOut
import com.few.api.domain.article.usecase.dto.WriterDetail
Expand All @@ -9,6 +10,8 @@ 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 @@ -19,6 +22,8 @@ class ReadArticleUseCase(
private val articleDao: ArticleDao,
private val readArticleWriterRecordService: ReadArticleWriterRecordService,
private val browseArticleProblemsService: BrowseArticleProblemsService,
private val articleViewHisDao: ArticleViewHisDao,
private val articleViewHisAsyncHandler: ArticleViewHisAsyncHandler,
) {

@Transactional(readOnly = true)
Expand All @@ -31,9 +36,14 @@ class ReadArticleUseCase(
readArticleWriterRecordService.execute(query) ?: throw NotFoundException("writer.notfound.id")
}

val problemIds = BrowseArticleProblemIdsInDto(articleRecord.articleId).let { query: BrowseArticleProblemIdsInDto ->
browseArticleProblemsService.execute(query)
}
val problemIds =
BrowseArticleProblemIdsInDto(articleRecord.articleId).let { query: BrowseArticleProblemIdsInDto ->
browseArticleProblemsService.execute(query)
}

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

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

return ReadArticleUseCaseOut(
id = articleRecord.articleId,
Expand All @@ -46,7 +56,8 @@ class ReadArticleUseCase(
content = articleRecord.content,
problemIds = problemIds.problemIds,
category = CategoryType.convertToDisplayName(articleRecord.category),
createdAt = articleRecord.createdAt
createdAt = articleRecord.createdAt,
views = views
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package com.few.api.domain.article.usecase.dto

data class ReadArticleUseCaseIn(
val articleId: Long,
val memberId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ data class ReadArticleUseCaseOut(
val problemIds: List<Long>,
val category: String,
val createdAt: LocalDateTime,
val views: Long,
)

data class WriterDetail(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ArticleController(
@Min(value = 1, message = "{min.id}")
articleId: Long,
): ApiResponse<ApiResponse.SuccessBody<ReadArticleResponse>> {
val useCaseOut = ReadArticleUseCaseIn(articleId).let { useCaseIn: ReadArticleUseCaseIn ->
val useCaseOut = ReadArticleUseCaseIn(articleId = articleId, memberId = 0L).let { useCaseIn: ReadArticleUseCaseIn -> //TODO: membberId검토
readArticleUseCase.execute(useCaseIn)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class ReadArticleResponse(
val problemIds: List<Long>,
val category: String,
val createdAt: LocalDateTime,
val views: Long,
) {
constructor(
useCaseOut: ReadArticleUseCaseOut,
Expand All @@ -26,7 +27,8 @@ data class ReadArticleResponse(
content = useCaseOut.content,
problemIds = useCaseOut.problemIds,
category = useCaseOut.category,
createdAt = useCaseOut.createdAt
createdAt = useCaseOut.createdAt,
views = useCaseOut.views
)
}

Expand Down
8 changes: 8 additions & 0 deletions api/src/main/resources/application-client-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ discord:
queue-capacity: 30
wait-for-tasks-to-complete-on-shutdown: true
await-termination-seconds: 60

database:
thread-pool:
core-pool-size: 10
max-pool-size: 30
queue-capacity: 70
wait-for-tasks-to-complete-on-shutdown: true
await-termination-seconds: 60
8 changes: 8 additions & 0 deletions api/src/main/resources/application-client-prd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ discord:
queue-capacity: ${DISCORD_THREAD_POOL_QUEUE_CAPACITY:30}
wait-for-tasks-to-complete-on-shutdown: ${DISCORD_THREAD_POOL_WAIT_FOR_TASKS_TO_COMPLETE_ON_SHUTDOWN:true}
await-termination-seconds: ${DISCORD_THREAD_POOL_AWAIT_TERMINATION_SECONDS:60}

database:
thread-pool:
core-pool-size: 10
max-pool-size: 30
queue-capacity: 70
wait-for-tasks-to-complete-on-shutdown: true
await-termination-seconds: 60
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
package com.few.api.domain.article.usecase

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
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.*

import java.net.URL
import java.time.LocalDateTime

class ReadArticleUseCaseTest : BehaviorSpec({
val log = KotlinLogging.logger {}

lateinit var articleDao: ArticleDao
lateinit var readArticleWriterRecordService: ReadArticleWriterRecordService
lateinit var browseArticleProblemsService: BrowseArticleProblemsService
lateinit var useCase: ReadArticleUseCase
val useCaseIn = ReadArticleUseCaseIn(articleId = 1L)
lateinit var articleViewHisDao: ArticleViewHisDao
lateinit var articleViewHisAsyncHandler: ArticleViewHisAsyncHandler
val useCaseIn = ReadArticleUseCaseIn(articleId = 1L, memberId = 1L)

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

given("아티클 조회 요청이 온 상황에서") {
Expand All @@ -52,13 +64,19 @@ 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 {
log.debug { "Inserting article view history asynchronously" }
}

then("아티클이 정상 조회된다") {
useCase.execute(useCaseIn)

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()) }
}
}

Expand Down
Loading

0 comments on commit 7ffe45c

Please sign in to comment.