From bfe0390c78903ef8f1841b5d5077a2553652fdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=8F=84=EC=A7=84?= Date: Thu, 30 Nov 2023 09:45:38 +0900 Subject: [PATCH] =?UTF-8?q?[feature]=20expired=20banner=20migration=20job?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatda/scheduled/ExpiredBannerBatchJob.kt | 72 +++++++++++++++++ .../eatda/scheduled/ScheduleJobConfig.kt | 17 ++++ src/main/resources/application.yml | 8 ++ .../scheduled/ExpiredBannerBatchJobTest.kt | 79 +++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 src/main/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJob.kt create mode 100644 src/main/kotlin/com/mjucow/eatda/scheduled/ScheduleJobConfig.kt create mode 100644 src/test/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJobTest.kt diff --git a/src/main/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJob.kt b/src/main/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJob.kt new file mode 100644 index 0000000..3e2f19b --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJob.kt @@ -0,0 +1,72 @@ +package com.mjucow.eatda.scheduled + +import com.mjucow.eatda.jooq.tables.Banner.BANNER +import com.mjucow.eatda.jooq.tables.records.ExpiredBannerRecord +import org.jooq.DSLContext +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.support.TransactionTemplate +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +@Component +class ExpiredBannerBatchJob( + private val db: DSLContext, + private val transactionTemplate: TransactionTemplate, +) { + @Scheduled(cron = "0 0 22 * * *", zone = "Asia/Seoul") // 매일 오후 10시 동작 + fun scheduleTaskUsingCronExpression() { + val now = Instant.now() + transactionTemplate.execute { + val expiredBanners = findAllExpiredBanners(now) + if (expiredBanners.isEmpty()) { + return@execute + } + + expiredBannerBulkInsert(expiredBanners) + + bulkDeleteBanners(expiredBanners) + } + } + + private fun findAllExpiredBanners(now: Instant): List { + return db + .select(BANNER.ID, BANNER.LINK, BANNER.IMAGE_ADDRESS, BANNER.EXPIRED_AT) + .from(BANNER) + .where( + BANNER.EXPIRED_AT.isNotNull + .and(BANNER.EXPIRED_AT.lessOrEqual(LocalDateTime.ofInstant(now, ZONE_ID))) + ) + .fetch() + .into(ExpiredTargetBanner::class.java) + .toList() + } + + private fun expiredBannerBulkInsert(expireTargetBanners: List) { + val expiredBanners = expireTargetBanners.map { banner -> + ExpiredBannerRecord().apply { + this.link = banner.link + this.imageAddress = banner.imageAddress + this.expiredAt = banner.expiredAt + } + } + db.batchInsert(expiredBanners).execute() + } + + private fun bulkDeleteBanners(expiredBanners: List) { + val deletedBannerIds = expiredBanners.map { it.id } + db.deleteFrom(BANNER).where(BANNER.ID.`in`(deletedBannerIds)).execute() + } + + data class ExpiredTargetBanner( + val id: Long, + val link: String, + val imageAddress: String, + val expiredAt: LocalDateTime, + ) + + companion object { + val ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul") + } +} diff --git a/src/main/kotlin/com/mjucow/eatda/scheduled/ScheduleJobConfig.kt b/src/main/kotlin/com/mjucow/eatda/scheduled/ScheduleJobConfig.kt new file mode 100644 index 0000000..7cac8fd --- /dev/null +++ b/src/main/kotlin/com/mjucow/eatda/scheduled/ScheduleJobConfig.kt @@ -0,0 +1,17 @@ +package com.mjucow.eatda.scheduled + +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableScheduling + +@Configuration +@EnableScheduling +class ScheduleJobConfig { +// @Bean +// fun taskScheduler(): TaskScheduler { +// val threadPoolTaskScheduler = ThreadPoolTaskScheduler().apply { +// poolSize = 5 +// setThreadNamePrefix("ThreadPoolTaskScheduler") +// } +// return threadPoolTaskScheduler +// } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2063358..ba9adbf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,10 @@ spring: generate-ddl: false hibernate: ddl-auto: validate + task: + scheduling: + pool: + size: 1 web.resources.add-mappings: false liquibase: @@ -56,6 +60,10 @@ spring: generate-ddl: false hibernate: ddl-auto: none + task: + scheduling: + pool: + size: 5 datasource: driver-class-name: org.postgresql.Driver diff --git a/src/test/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJobTest.kt b/src/test/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJobTest.kt new file mode 100644 index 0000000..46e8cfa --- /dev/null +++ b/src/test/kotlin/com/mjucow/eatda/scheduled/ExpiredBannerBatchJobTest.kt @@ -0,0 +1,79 @@ +package com.mjucow.eatda.scheduled + +import com.mjucow.eatda.domain.AbstractDataTest +import com.mjucow.eatda.domain.banner.entity.objectmother.BannerMother +import com.mjucow.eatda.jooq.tables.ExpiredBanner.EXPIRED_BANNER +import com.mjucow.eatda.jooq.tables.records.BannerRecord +import com.mjucow.eatda.jooq.tables.records.ExpiredBannerRecord +import com.mjucow.eatda.persistence.banner.BannerRepository +import org.assertj.core.api.Assertions +import org.jooq.DSLContext +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDateTime +import java.util.stream.IntStream + +@Import(value = [ExpiredBannerBatchJob::class]) +class ExpiredBannerBatchJobTest : AbstractDataTest() { + @Autowired + lateinit var db: DSLContext + + @Autowired + lateinit var bannerRepository: BannerRepository + + @Autowired + lateinit var expiredBannerBatchJob: ExpiredBannerBatchJob + + @DisplayName("배치할 거 없으면 따로 동작 안하기") + @Test + fun test1() { + // given + + // when + expiredBannerBatchJob.scheduleTaskUsingCronExpression() + + // then + val createdExpiredBanners = findAllExpiredBanner() + Assertions.assertThat(createdExpiredBanners).isEmpty() + } + + @DisplayName("배치할 거 있으면 expiredBanner로 데이터 복사하기") + @Test + fun test2() { + // given + val now = LocalDateTime.now(ZONE_ID) + val expiredAt = now.minusDays(1) + val count = 3 + db.batchInsert( + IntStream + .range(0, count) + .mapToObj { + BannerRecord().apply { + this.imageAddress = BannerMother.IMAGE_ADDRESS + this.link = BannerMother.LINK + this.expiredAt = expiredAt + this.createdAt = now + this.updatedAt = now + } + }.toList() + ).execute() + + // when + expiredBannerBatchJob.scheduleTaskUsingCronExpression() + + // then + val createdExpiredBanners = findAllExpiredBanner() + Assertions.assertThat(createdExpiredBanners).hasSize(count) + Assertions.assertThat(bannerRepository.findAll()).isEmpty() + } + + private fun findAllExpiredBanner(): List { + return db.selectQuery(EXPIRED_BANNER).toList() + } + + companion object { + val ZONE_ID = ExpiredBannerBatchJob.ZONE_ID + } +}