From 22d9abb31f502870b43463a34c1dd1f8cc9e9eb4 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sun, 15 Sep 2024 22:54:22 +0900 Subject: [PATCH] =?UTF-8?q?[Fix/#401]=20=ED=95=98=EB=A3=A8=EC=95=88?= =?UTF-8?q?=EC=97=90=20=EC=9E=AC=EA=B5=AC=EB=8F=85=EC=8B=9C=20=EC=95=84?= =?UTF-8?q?=ED=8B=B0=ED=81=B4=20=EC=A4=91=EB=B3=B5=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=84=ED=96=89=EB=A5=A0=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#402)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 구독 발송 시간 컬럼 추가 * refactor: 아티클 전송 배치에 발송 시간 추가 * refactor: selectSubscriptionTimeRecordQuery 추가 및 전송 후 수정시 modified at, send at 추가 * refactor: SUBSCRIPTION MEMBER ID WORKBOOK ID Case 추가 * refactor: WorkbookSubscriptionAfterCompletionEventListener에 트랜잭션 제거 * refactor: 동일 날짜에 재구독시 아티클 전송하지 않도록 수정 --- .../repo/dao/subscription/SubscriptionDao.kt | 32 ++++++++++-- .../query/SelectSubscriptionQuery.kt | 6 +++ .../record/SubscriptionTimeRecord.kt | 11 +++++ .../SubscriptionDaoExplainGenerateTest.kt | 14 ++++++ .../few/api/domain/common/lock/LockAspect.kt | 49 ++++++++++++++----- ...ubscriptionAfterCompletionEventListener.kt | 3 -- .../SendWorkbookArticleAsyncHandler.kt | 29 +++++++++-- .../writer/WorkBookSubscriberWriter.kt | 4 ++ .../V1.00.0.24__add_subscription_send_at.sql | 2 + 9 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectSubscriptionQuery.kt create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionTimeRecord.kt create mode 100644 data/db/migration/entity/V1.00.0.24__add_subscription_send_at.sql diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt index fa7948c67..693a60945 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt @@ -2,13 +2,11 @@ package com.few.api.repo.dao.subscription import com.few.api.repo.dao.subscription.command.* import com.few.api.repo.dao.subscription.query.* -import com.few.api.repo.dao.subscription.record.WorkbookSubscriptionStatus -import com.few.api.repo.dao.subscription.record.CountAllSubscriptionStatusRecord -import com.few.api.repo.dao.subscription.record.MemberWorkbookSubscriptionStatusRecord -import com.few.api.repo.dao.subscription.record.SubscriptionSendStatusRecord +import com.few.api.repo.dao.subscription.record.* import jooq.jooq_dsl.Tables.MAPPING_WORKBOOK_ARTICLE import jooq.jooq_dsl.Tables.SUBSCRIPTION import jooq.jooq_dsl.tables.MappingWorkbookArticle +import jooq.jooq_dsl.tables.Subscription import org.jooq.DSLContext import org.jooq.impl.DSL import org.springframework.stereotype.Repository @@ -54,6 +52,7 @@ class SubscriptionDao( dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, null as LocalDateTime?) .set(SUBSCRIPTION.UNSUBS_OPINION, null as String?) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) @@ -65,6 +64,7 @@ class SubscriptionDao( fun updateDeletedAtInWorkbookSubscriptionCommand(command: UpdateDeletedAtInWorkbookSubscriptionCommand) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) + .set(Subscription.SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) .set(SUBSCRIPTION.UNSUBS_OPINION, command.opinion) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) @@ -140,6 +140,7 @@ class SubscriptionDao( fun updateDeletedAtInAllSubscriptionCommand(command: UpdateDeletedAtInAllSubscriptionCommand) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) .set(SUBSCRIPTION.UNSUBS_OPINION, command.opinion) // TODO: opinion row 마다 중복 해결 .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) @@ -191,6 +192,8 @@ class SubscriptionDao( command: UpdateArticleProgressCommand, ) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.PROGRESS, SUBSCRIPTION.PROGRESS.add(1)) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .set(SUBSCRIPTION.SEND_AT, LocalDateTime.now()) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) @@ -202,6 +205,8 @@ class SubscriptionDao( fun updateLastArticleProgressCommand(command: UpdateLastArticleProgressCommand) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .set(SUBSCRIPTION.SEND_AT, LocalDateTime.now()) .set(SUBSCRIPTION.UNSUBS_OPINION, command.opinion) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) @@ -258,4 +263,23 @@ class SubscriptionDao( .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.`in`(command.workbookIds)) + + fun selectSubscriptionTimeRecord( + query: SelectSubscriptionQuery, + ): SubscriptionTimeRecord? { + return selectSubscriptionTimeRecordQuery(query) + .fetchOneInto(SubscriptionTimeRecord::class.java) + } + + fun selectSubscriptionTimeRecordQuery(query: SelectSubscriptionQuery) = + dslContext.select( + SUBSCRIPTION.MEMBER_ID.`as`(SubscriptionTimeRecord::memberId.name), + SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(SubscriptionTimeRecord::workbookId.name), + SUBSCRIPTION.CREATED_AT.`as`(SubscriptionTimeRecord::createdAt.name), + SUBSCRIPTION.MODIFIED_AT.`as`(SubscriptionTimeRecord::modifiedAt.name), + SUBSCRIPTION.SEND_AT.`as`(SubscriptionTimeRecord::sendAt.name) + ) + .from(SUBSCRIPTION) + .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) + .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(query.workbookId)) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectSubscriptionQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectSubscriptionQuery.kt new file mode 100644 index 000000000..69179ceba --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectSubscriptionQuery.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.subscription.query + +data class SelectSubscriptionQuery( + val memberId: Long, + val workbookId: Long, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionTimeRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionTimeRecord.kt new file mode 100644 index 000000000..ba705342a --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionTimeRecord.kt @@ -0,0 +1,11 @@ +package com.few.api.repo.dao.subscription.record + +import java.time.LocalDateTime + +data class SubscriptionTimeRecord( + val memberId: Long, + val workbookId: Long, + val createdAt: LocalDateTime, + val modifiedAt: LocalDateTime, + val sendAt: LocalDateTime?, +) \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt index c918e0a49..824b38bf0 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt @@ -201,4 +201,18 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "selectAllSubscriptionSendStatusQueryExplain") } + + @Test + fun selectSubscriptionTimeRecordQueryExplain() { + val query = subscriptionDao.selectSubscriptionTimeRecordQuery( + SelectSubscriptionQuery( + memberId = 1L, + workbookId = 1L + ) + ) + + val explain = ExplainGenerator.execute(dslContext, query) + + ResultGenerator.execute(query, explain, "selectSubscriptionTimeRecordQueryExplain") + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt b/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt index 6ceb5015e..05a51c173 100644 --- a/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt +++ b/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt @@ -27,19 +27,33 @@ class LockAspect( getLockFor(joinPoint).run { when (this.identifier) { LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { - val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn - getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + getSubscriptionMemberIdAndWorkBookIdLockCase(joinPoint) } } } } + private fun getSubscriptionMemberIdAndWorkBookIdLockCase(joinPoint: JoinPoint) { + if (joinPoint.args[0] is SubscribeWorkbookUseCaseIn) { + val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn + getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + } else { + val memberId = joinPoint.args[0] as Long + val workbookId = joinPoint.args[1] as Long + getSubscriptionMemberIdAndWorkBookIdLock(memberId, workbookId) + } + } + private fun getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) { - subscriptionDao.getLock(useCaseIn.memberId, useCaseIn.workbookId).run { + getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn.memberId, useCaseIn.workbookId) + } + + private fun getSubscriptionMemberIdAndWorkBookIdLock(memberId: Long, workbookId: Long) { + subscriptionDao.getLock(memberId, workbookId).run { if (!this) { - throw IllegalStateException("Already in progress for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}") + throw IllegalStateException("Already in progress for $memberId's subscription to $workbookId") } - log.debug { "Lock acquired for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" } + log.debug { "Lock acquired for $memberId's subscription to $workbookId" } } } @@ -48,8 +62,7 @@ class LockAspect( getLockFor(joinPoint).run { when (this.identifier) { LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { - val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn - releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + releaseSubscriptionMemberIdAndWorkBookIdLockCase(joinPoint) } } } @@ -60,8 +73,7 @@ class LockAspect( getLockFor(joinPoint).run { when (this.identifier) { LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { - val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn - releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + releaseSubscriptionMemberIdAndWorkBookIdLockCase(joinPoint) } } } @@ -70,8 +82,23 @@ class LockAspect( private fun getLockFor(joinPoint: JoinPoint) = (joinPoint.signature as MethodSignature).method.getAnnotation(LockFor::class.java) + private fun releaseSubscriptionMemberIdAndWorkBookIdLockCase(joinPoint: JoinPoint) { + if (joinPoint.args[0] is SubscribeWorkbookUseCaseIn) { + val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn + releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + } else { + val memberId = joinPoint.args[0] as Long + val workbookId = joinPoint.args[1] as Long + releaseSubscriptionMemberIdAndWorkBookIdLock(memberId, workbookId) + } + } + private fun releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) { - subscriptionDao.releaseLock(useCaseIn.memberId, useCaseIn.workbookId) - log.debug { "Lock released for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" } + releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn.memberId, useCaseIn.workbookId) + } + + private fun releaseSubscriptionMemberIdAndWorkBookIdLock(memberId: Long, workbookId: Long) { + subscriptionDao.releaseLock(memberId, workbookId) + log.debug { "Lock released for $memberId's subscription to $workbookId" } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/event/WorkbookSubscriptionAfterCompletionEventListener.kt b/api/src/main/kotlin/com/few/api/domain/subscription/event/WorkbookSubscriptionAfterCompletionEventListener.kt index 13796ab42..befafe8bc 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/event/WorkbookSubscriptionAfterCompletionEventListener.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/event/WorkbookSubscriptionAfterCompletionEventListener.kt @@ -3,8 +3,6 @@ package com.few.api.domain.subscription.event import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent import com.few.api.domain.subscription.handler.SendWorkbookArticleAsyncHandler import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.event.TransactionPhase import org.springframework.transaction.event.TransactionalEventListener @@ -14,7 +12,6 @@ class WorkbookSubscriptionAfterCompletionEventListener( ) { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) - @Transactional(propagation = Propagation.REQUIRES_NEW) fun handleEvent(event: WorkbookSubscriptionEvent) { sendWorkbookArticleAsyncHandler.sendWorkbookArticle( event.memberId, diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt index 415c9462e..9bf3d27c3 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt @@ -1,6 +1,8 @@ package com.few.api.domain.subscription.handler import com.few.api.config.DatabaseAccessThreadPoolConfig.Companion.DATABASE_ACCESS_POOL +import com.few.api.domain.common.lock.LockFor +import com.few.api.domain.common.lock.LockIdentifier import com.few.api.domain.subscription.service.SubscriptionArticleService import com.few.api.domain.subscription.service.SubscriptionMemberService import com.few.api.domain.subscription.service.SubscriptionEmailService @@ -10,11 +12,14 @@ import com.few.api.exception.common.NotFoundException import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateArticleProgressCommand import com.few.api.repo.dao.subscription.command.UpdateLastArticleProgressCommand +import com.few.api.repo.dao.subscription.query.SelectSubscriptionQuery import com.few.data.common.code.CategoryType import com.few.email.service.article.dto.Content import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional import java.time.LocalDate @Component @@ -28,8 +33,22 @@ class SendWorkbookArticleAsyncHandler( private val log = KotlinLogging.logger {} @Async(value = DATABASE_ACCESS_POOL) + @LockFor(identifier = LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID) + @Transactional(propagation = Propagation.REQUIRES_NEW) fun sendWorkbookArticle(memberId: Long, workbookId: Long, articleDayCol: Byte) { val date = LocalDate.now() + + subscriptionDao.selectSubscriptionTimeRecord( + SelectSubscriptionQuery( + memberId = memberId, + workbookId = workbookId + ) + )?.let { + if (it.sendAt?.isAfter(date.atStartOfDay()) == true) { + return + } + } + val memberEmail = memberService.readMemberEmail(ReadMemberEmailInDto(memberId))?.email ?: throw NotFoundException("member.notfound.id") val article = articleService.readArticleIdByWorkbookIdAndDay( @@ -70,18 +89,18 @@ class SendWorkbookArticleAsyncHandler( ) )?.lastArticleId ?: throw NotFoundException("workbook.notfound.id") - if (article.id == lastDayArticleId) { + if (article.id != lastDayArticleId) { subscriptionDao.updateArticleProgress( UpdateArticleProgressCommand( - workbookId, - memberId + memberId, + workbookId ) ) } else { subscriptionDao.updateLastArticleProgress( UpdateLastArticleProgressCommand( - workbookId, - memberId + memberId, + workbookId ) ) } diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt index df988f43b..76b340e1a 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt @@ -89,6 +89,8 @@ class WorkBookSubscriberWriter( updateQueries.add( dslContext.update(Subscription.SUBSCRIPTION) .set(Subscription.SUBSCRIPTION.PROGRESS, updateTargetMemberRecord.updatedProgress) + .set(Subscription.SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .set(Subscription.SUBSCRIPTION.SEND_AT, LocalDateTime.now()) .where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(updateTargetMemberRecord.memberId)) .and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(updateTargetMemberRecord.targetWorkBookId)) ) @@ -101,6 +103,8 @@ class WorkBookSubscriberWriter( receiveLastDayQueries.add( dslContext.update(Subscription.SUBSCRIPTION) .set(Subscription.SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) + .set(Subscription.SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .set(Subscription.SUBSCRIPTION.SEND_AT, LocalDateTime.now()) .set(Subscription.SUBSCRIPTION.UNSUBS_OPINION, "receive.all") .where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(receiveLastDayMember.memberId)) .and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(receiveLastDayMember.targetWorkBookId)) diff --git a/data/db/migration/entity/V1.00.0.24__add_subscription_send_at.sql b/data/db/migration/entity/V1.00.0.24__add_subscription_send_at.sql new file mode 100644 index 000000000..9926c5486 --- /dev/null +++ b/data/db/migration/entity/V1.00.0.24__add_subscription_send_at.sql @@ -0,0 +1,2 @@ +-- 구독 발송 시간 컬럼 추가 +ALTER TABLE SUBSCRIPTION ADD COLUMN send_at TIMESTAMP;