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/#215] article_view_his 테이블 추가 및 조회수 응답 추가 #216

Merged
merged 20 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3c02659
feat: flyway V1.00.0.7 배포(ARTICLE_VIEW_HIS 테이블 추가)
hun-ca Jul 17, 2024
977de9a
feat: article view history 저장 로직 추가
hun-ca Jul 17, 2024
1334ccf
feat: article 조회시 views 필드 추가
hun-ca Jul 17, 2024
390b898
test: article 조회수 추가 관련 test 코드 수정
hun-ca Jul 17, 2024
2304654
feat: ARTICLE_VIEW_HIS 테이블 인덱스 추가(article_mst_id 컬럼)
hun-ca Jul 17, 2024
29ca0fb
fix: 인덱스 테이블 명 수정
hun-ca Jul 17, 2024
bfaebe6
refactor: DAO 메소드 명 수정
hun-ca Jul 18, 2024
362999e
refactor: 아티클 뷰와 아티클 도메인 통합
hun-ca Jul 18, 2024
9c17140
refactor: DAO 빈 지정 stereotype 변경(@Component -> @Repository)
hun-ca Jul 18, 2024
abde0c3
refactor: 조회수 결정에 대한 로직을 DAO -> UC로 이동
hun-ca Jul 18, 2024
89a188f
chore: member Id 사용에 대한 TODO 주석 추가
hun-ca Jul 18, 2024
a8342ba
feat: 아티클 조회 기록 Insert과정을 다른 쓰레드에서 수행하도록 변경
hun-ca Jul 18, 2024
21c0ad4
fix: add ArticleViewHisAsyncEvent
hun-ca Jul 18, 2024
4ce6ac9
test: ReadArticleUseCaseTest 수정반영
hun-ca Jul 18, 2024
51671e4
refactor: SubscribeWorkbookUseCaseTest 에서 print 삭제 및 log 적용
hun-ca Jul 18, 2024
a76ed19
refactor: rename ArticleViewHisAsyncHandler
hun-ca Jul 18, 2024
dc76e36
test: application-test.yaml에 database async thread pool 설정 추가
hun-ca Jul 18, 2024
7bac2fc
fix: 테스트 통과하지 못하는 문제 해결
belljun3395 Jul 19, 2024
ba64abd
Merge remote-tracking branch 'origin/main' into feat/#215_hunca
hun-ca Jul 19, 2024
3348881
feat: 아티클 조회응답 바디 views 필드 추가
hun-ca Jul 19, 2024
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
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
Comment on lines +98 to +104
Copy link
Member Author

Choose a reason for hiding this comment

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

각 모듈별로 jooq 필드시 생성되는 .sql 파일이 gitignore되어있었는데, 최상위 위치에 통합관리하도록 변경했습니다

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
Copy link
Member Author

@hun-ca hun-ca Jul 17, 2024

Choose a reason for hiding this comment

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

종준님은 DAO에 @Repository 쓰셨던데 저는 @Component 썼거든요 뭐가 더 어울릴까요? 통일하는게 좋을거 같아서

Copy link
Collaborator

Choose a reason for hiding this comment

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

스크린샷 2024-07-18 오전 12 21 48 스크린샷 2024-07-18 오전 12 21 17 우선 블로그 글이긴 한데 저도 저렇게 알고 있어서 `@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()
}
Comment on lines +14 to +23
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_his 테이블 insert. articleId에 인덱스 추가해야겠다...


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()
}
Comment on lines +10 to +37
Copy link
Member Author

Choose a reason for hiding this comment

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

디코 훅 쓰레드 풀이랑 똑같이 디비 접근용 쓰레드 풀 생성. 추후 이걸 활용할 것들이 많아보여서 네이밍을 좀 일반적이게 했어요(조회수랑 관련없이)

}
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.config.DatabaseAccessThreadPoolConfig.Companion.DATABASE_ACCESS_POOL
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,8 +10,13 @@ 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.command.ArticleViewHisCommand
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 io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

Expand All @@ -19,8 +25,11 @@ class ReadArticleUseCase(
private val articleDao: ArticleDao,
private val readArticleWriterRecordService: ReadArticleWriterRecordService,
private val browseArticleProblemsService: BrowseArticleProblemsService,
private val articleViewHisDao: ArticleViewHisDao,
) {

private val log = KotlinLogging.logger {}

@Transactional(readOnly = true)
fun execute(useCaseIn: ReadArticleUseCaseIn): ReadArticleUseCaseOut {
val articleRecord = SelectArticleRecordQuery(useCaseIn.articleId).let { query: SelectArticleRecordQuery ->
Expand All @@ -31,9 +40,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
Copy link
Member Author

Choose a reason for hiding this comment

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

조회수 가져온 뒤에 우선 + 1 하도록 변경


insertArticleViewHisAsync(useCaseIn.articleId, useCaseIn.memberId)

return ReadArticleUseCaseOut(
id = articleRecord.articleId,
Expand All @@ -46,7 +60,19 @@ class ReadArticleUseCase(
content = articleRecord.content,
problemIds = problemIds.problemIds,
category = CategoryType.convertToDisplayName(articleRecord.category),
createdAt = articleRecord.createdAt
createdAt = articleRecord.createdAt,
views = views
)
}

@Async(value = DATABASE_ACCESS_POOL)
@Transactional
fun insertArticleViewHisAsync(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" }
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Insert 는 DATABASE_ACCESS_POOL 풀에서 수행하도록 변경. 어차피 다른 쓰레드에서 수행되기 때문에 기존 아티클을 조회하던 쓰레드와 트랜잭션 컨텍스트를 공유하지 않고, 따라서 트랜잭션 전파 레벨 지정은 필요 없음. (전파 자체가 안됨, new transaction)

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 로직은 일단 UC 내부에 넣어뒀는데 서비스로 뺴기도 애매한게, 같은 도메인이여서,, 좋은 생각있음 공유좀요

Copy link
Collaborator

@belljun3395 belljun3395 Jul 18, 2024

Choose a reason for hiding this comment

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

해당 로직은 일단 UC 내부에 넣어뒀는데 서비스로 뺴기도 애매한게, 같은 도메인이여서,, 좋은 생각있음 공유좀요

스크린샷 2024-07-19 오전 12 24 33

저는 요렇게 했습니다.

UC 내부에 넣어뒀는데

아 추가로 같은 클래스내의 메서드는 비동기 처리가 가능하지 않으니 무조건 빼야하긴 하겠네요!

Copy link
Member Author

Choose a reason for hiding this comment

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

일단 handler로 함...

Copy link
Collaborator

@belljun3395 belljun3395 Jul 19, 2024

Choose a reason for hiding this comment

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

handler 가 안보이는데 혹시 어디 있을까요?
아아 죄송합니다..
확인했어요!

}
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
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
Comment on lines +17 to +23
Copy link
Member Author

Choose a reason for hiding this comment

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

이건 VM 환경변수로 뺼까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

이거 설정하면서 든 생각이, 만약 조회가 엄청 많이 발생하는 상황에선 max 개수인 30 + queue 대기 70 해서 100 명 까지만 Insert가 될 거 같음...

Copy link
Collaborator

Choose a reason for hiding this comment

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

그럼 databaseAccessThreadPool의 setRejectedExecutionHandler 부분을 수정하면 되지 않을까요?
제가 디스코드에 해둔 것은 단순히 실패 로그를 남기는 것 뿐인데 다른 기본 정책들도 있더라고요
예를들어 CallerRunsPolicy 요런 정책이 있습니다.

Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ 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.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
Expand All @@ -22,13 +21,20 @@ class ReadArticleUseCaseTest : BehaviorSpec({
lateinit var readArticleWriterRecordService: ReadArticleWriterRecordService
lateinit var browseArticleProblemsService: BrowseArticleProblemsService
lateinit var useCase: ReadArticleUseCase
val useCaseIn = ReadArticleUseCaseIn(articleId = 1L)
lateinit var articleViewHisDao: ArticleViewHisDao
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>()
useCase = ReadArticleUseCase(
articleDao,
readArticleWriterRecordService,
browseArticleProblemsService,
articleViewHisDao
)
}

given("아티클 조회 요청이 온 상황에서") {
Expand All @@ -52,6 +58,8 @@ class ReadArticleUseCaseTest : BehaviorSpec({
every { articleDao.selectArticleRecord(any()) } returns record
every { readArticleWriterRecordService.execute(any()) } returns writerSvcOutDto
every { browseArticleProblemsService.execute(any()) } returns probSvcOutDto
every { articleViewHisDao.insertArticleViewHis(any()) } just Runs
every { articleViewHisDao.countArticleViews(any()) } returns 1L

then("아티클이 정상 조회된다") {
useCase.execute(useCaseIn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class ArticleControllerTest : ControllerTestSpec() {
val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/{articleId}").build().toUriString()
// set usecase mock
val articleId = 1L
`when`(readArticleUseCase.execute(ReadArticleUseCaseIn(articleId))).thenReturn(
val memberId = 1L
`when`(readArticleUseCase.execute(ReadArticleUseCaseIn(articleId, memberId))).thenReturn(
ReadArticleUseCaseOut(
id = 1L,
writer = WriterDetail(
Expand All @@ -81,7 +82,8 @@ class ArticleControllerTest : ControllerTestSpec() {
content = CategoryType.fromCode(0)!!.name,
problemIds = listOf(1L, 2L, 3L),
category = "경제",
createdAt = LocalDateTime.now()
createdAt = LocalDateTime.now(),
views = 1L
)
)

Expand Down Expand Up @@ -119,7 +121,9 @@ class ArticleControllerTest : ControllerTestSpec() {
PayloadDocumentation.fieldWithPath("data.category")
.fieldWithString("아티클 카테고리"),
PayloadDocumentation.fieldWithPath("data.createdAt")
.fieldWithString("아티클 생성일")
.fieldWithString("아티클 생성일"),
PayloadDocumentation.fieldWithPath("data.views")
.fieldWithString("아티클 조회수")
)
)
).build()
Expand Down
2 changes: 0 additions & 2 deletions batch/.gitignore

This file was deleted.

2 changes: 0 additions & 2 deletions data/.gitignore

This file was deleted.

13 changes: 13 additions & 0 deletions data/db/migration/entity/V1.00.0.7__article_view_his_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- 아티클 조회수 저장 테이블
CREATE TABLE ARTICLE_VIEW_HIS
(
id BIGINT NOT NULL AUTO_INCREMENT,
article_mst_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (id)
);
Comment on lines +2 to +10
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_HIS 테이블 스키마


-- [인덱스 추가] --
CREATE INDEX article_view_his_idx1 ON ARTICLE_VIEW_HIS (article_mst_id);
2 changes: 0 additions & 2 deletions email/.gitignore

This file was deleted.

2 changes: 0 additions & 2 deletions storage/.gitignore

This file was deleted.

Loading