diff --git a/domain/generator/src/main/kotlin/com/few/generator/service/ContentsCommonGenerationService.kt b/domain/generator/src/main/kotlin/com/few/generator/service/ContentsCommonGenerationService.kt index 276e7f0d9..80ce2d1a8 100644 --- a/domain/generator/src/main/kotlin/com/few/generator/service/ContentsCommonGenerationService.kt +++ b/domain/generator/src/main/kotlin/com/few/generator/service/ContentsCommonGenerationService.kt @@ -2,15 +2,32 @@ package com.few.generator.service import com.few.common.domain.Category import com.few.common.domain.Region +import com.few.common.exception.BadRequestException +import com.few.generator.config.GroupingProperties +import com.few.generator.domain.vo.GenDetail +import com.few.generator.domain.vo.GroupGenProcessingResult +import com.few.generator.service.specifics.groupgen.GenGroupper +import com.few.generator.service.specifics.groupgen.GroupContentGenerator +import com.few.generator.service.specifics.groupgen.KeywordExtractor import com.few.generator.support.jpa.GeneratorTransactional +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import kotlin.system.measureTimeMillis @Service class ContentsCommonGenerationService( protected val rawContentsService: RawContentsService, protected val provisioningService: ProvisioningService, protected val genService: GenService, + protected val applicationEventPublisher: ApplicationEventPublisher, + protected val groupingProperties: GroupingProperties, + protected val keywordExtractor: KeywordExtractor, + protected val genGrouper: GenGroupper, + protected val groupContentGenerator: GroupContentGenerator, ) { + protected val log = KotlinLogging.logger {} + /** * RawContents, ProvisioningContents, Gen 중 1개라도 실패시 rollback하기 위해 * 개별 트랜잭션으로 분리 @@ -25,4 +42,56 @@ class ContentsCommonGenerationService( val provisioningContent = provisioningService.createAndSave(rawContent) genService.createAndSave(rawContent, provisioningContent) } + + @GeneratorTransactional + open suspend fun createSingleGroupGen( + category: Category, + region: Region, + ): GroupGenProcessingResult { + val gens = + genService.findAllByCreatedAtTodayAndCategoryAndRegion( + category, + region, + ) + + if (gens.isEmpty()) { + throw BadRequestException("${region.name} Group Gen 생성 실패 - Cause: 카테고리 ${category.title}에 대한 Gen이 없습니다.") + } + + if (gens.size < groupingProperties.minGroupSize) { + throw BadRequestException( + "${region.name} Group Gen 생성 실패 - Cause: 카테고리 ${category.title}의 Gen 개수(${gens.size})가 최소 그룹 크기(${groupingProperties.minGroupSize})보다 작습니다.", + ) + } + + log.info { "${region.name} 카테고리 ${category.title}에서 ${gens.size}개 Gen 발견, 키워드 추출 시작" } + + // 배치로 ProvisioningContents 조회하여 N+1 쿼리 방지 + val provisioningContentsIds = gens.map { it.provisioningContentsId } + val provisioningContentsMap = + provisioningService + .findAllByIdIn(provisioningContentsIds) + .associateBy { it.id!! } + + // 키워드 추출 시간 측정 및 실행 (코루틴 버전) + val genDetails: List + val keywordExtractionTime = + measureTimeMillis { + genDetails = keywordExtractor.extractKeywordsFromGens(gens, provisioningContentsMap) + } + + log.info { "키워드 추출 완료, 그룹화 시작" } + + // 그룹화 수행 + val group = genGrouper.performGrouping(genDetails, category) + val validatedGroup = genGrouper.validateGroupSize(group) + + if (validatedGroup == null) { + throw BadRequestException("${region.name} Group Gen 생성 실패 - Cause: 카테고리 ${category.title} Gen Grouping 실패") + } + + // 그룹 콘텐츠 생성 + val result = groupContentGenerator.generateGroupContent(category, gens, validatedGroup, provisioningContentsMap, region) + return GroupGenProcessingResult(result, keywordExtractionTime, gens.size) + } } \ No newline at end of file diff --git a/domain/generator/src/main/kotlin/com/few/generator/usecase/AbstractGroupGenSchedulingUseCase.kt b/domain/generator/src/main/kotlin/com/few/generator/usecase/AbstractGroupGenSchedulingUseCase.kt index 8baf45a35..6b257b06f 100644 --- a/domain/generator/src/main/kotlin/com/few/generator/usecase/AbstractGroupGenSchedulingUseCase.kt +++ b/domain/generator/src/main/kotlin/com/few/generator/usecase/AbstractGroupGenSchedulingUseCase.kt @@ -4,22 +4,16 @@ import com.few.common.domain.Category import com.few.common.domain.Region import com.few.common.exception.BadRequestException import com.few.generator.config.GeneratorGsonConfig.Companion.GSON_BEAN_NAME -import com.few.generator.config.GroupingProperties import com.few.generator.domain.GroupGen -import com.few.generator.domain.vo.GenDetail -import com.few.generator.domain.vo.GroupGenProcessingResult import com.few.generator.event.ContentsSchedulingEvent +import com.few.generator.service.ContentsCommonGenerationService import com.few.generator.service.GenService -import com.few.generator.service.ProvisioningService -import com.few.generator.service.specifics.groupgen.GenGroupper -import com.few.generator.service.specifics.groupgen.GroupContentGenerator import com.few.generator.service.specifics.groupgen.GroupGenMetrics -import com.few.generator.service.specifics.groupgen.KeywordExtractor -import com.few.generator.support.jpa.GeneratorTransactional import com.google.gson.Gson import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking import org.springframework.beans.factory.annotation.Qualifier @@ -28,27 +22,23 @@ import java.time.LocalDateTime import java.util.concurrent.atomic.AtomicBoolean import kotlin.system.measureTimeMillis +@OptIn(ExperimentalCoroutinesApi::class) abstract class AbstractGroupGenSchedulingUseCase( protected val applicationEventPublisher: ApplicationEventPublisher, protected val genService: GenService, - protected val provisioningService: ProvisioningService, - protected val groupingProperties: GroupingProperties, @Qualifier(GSON_BEAN_NAME) protected val gson: Gson, protected val groupGenMetrics: GroupGenMetrics, - protected val keywordExtractor: KeywordExtractor, - protected val genGrouper: GenGroupper, - protected val groupContentGenerator: GroupContentGenerator, + protected val contentsCommonGenerationService: ContentsCommonGenerationService, ) { protected val log = KotlinLogging.logger {} protected val isRunning = AtomicBoolean(false) - protected val groupGenScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + protected val groupGenScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1)) abstract val region: Region abstract val regionName: String abstract val eventTitle: String - @GeneratorTransactional open fun execute() { if (!isRunning.compareAndSet(false, true)) { throw BadRequestException("$regionName group scheduling is already running. Please try again later.") @@ -139,7 +129,7 @@ abstract class AbstractGroupGenSchedulingUseCase( try { val internalResult = runBlocking(groupGenScope.coroutineContext) { - createGroupGenInternalWithMetrics(category) + contentsCommonGenerationService.createSingleGroupGen(category, region) } result = internalResult.groupGen keywordExtractionTime = internalResult.keywordExtractionTime @@ -170,54 +160,6 @@ abstract class AbstractGroupGenSchedulingUseCase( return result ?: throw BadRequestException("$regionName Group Gen 생성 실패 - Cause: Unknown (카테고리: ${category.title})") } - private suspend fun createGroupGenInternalWithMetrics(category: Category): GroupGenProcessingResult { - val gens = - genService.findAllByCreatedAtTodayAndCategoryAndRegion( - category, - region, - ) - - if (gens.isEmpty()) { - throw BadRequestException("$regionName Group Gen 생성 실패 - Cause: 카테고리 ${category.title}에 대한 Gen이 없습니다.") - } - - if (gens.size < groupingProperties.minGroupSize) { - throw BadRequestException( - "$regionName Group Gen 생성 실패 - Cause: 카테고리 ${category.title}의 Gen 개수(${gens.size})가 최소 그룹 크기(${groupingProperties.minGroupSize})보다 작습니다.", - ) - } - - log.info { "$regionName 카테고리 ${category.title}에서 ${gens.size}개 Gen 발견, 키워드 추출 시작" } - - // 배치로 ProvisioningContents 조회하여 N+1 쿼리 방지 - val provisioningContentsIds = gens.map { it.provisioningContentsId } - val provisioningContentsMap = - provisioningService - .findAllByIdIn(provisioningContentsIds) - .associateBy { it.id!! } - - // 키워드 추출 시간 측정 및 실행 (코루틴 버전) - val genDetails: List - val keywordExtractionTime = - measureTimeMillis { - genDetails = keywordExtractor.extractKeywordsFromGens(gens, provisioningContentsMap) - } - - log.info { "키워드 추출 완료, 그룹화 시작" } - - // 그룹화 수행 - val group = genGrouper.performGrouping(genDetails, category) - val validatedGroup = genGrouper.validateGroupSize(group) - - if (validatedGroup == null) { - throw BadRequestException("$regionName Group Gen 생성 실패 - Cause: 카테고리 ${category.title} Gen Grouping 실패") - } - - // 그룹 콘텐츠 생성 - val result = groupContentGenerator.generateGroupContent(category, gens, validatedGroup, provisioningContentsMap, region) - return GroupGenProcessingResult(result, keywordExtractionTime, gens.size) - } - private fun updateSuccessMetrics( category: Category, groupGen: GroupGen, diff --git a/domain/generator/src/main/kotlin/com/few/generator/usecase/GlobalGroupGenSchedulingUseCase.kt b/domain/generator/src/main/kotlin/com/few/generator/usecase/GlobalGroupGenSchedulingUseCase.kt index 03110aaaf..2e30f06a6 100644 --- a/domain/generator/src/main/kotlin/com/few/generator/usecase/GlobalGroupGenSchedulingUseCase.kt +++ b/domain/generator/src/main/kotlin/com/few/generator/usecase/GlobalGroupGenSchedulingUseCase.kt @@ -2,14 +2,10 @@ package com.few.generator.usecase import com.few.common.domain.Region import com.few.generator.config.GeneratorGsonConfig.Companion.GSON_BEAN_NAME -import com.few.generator.config.GroupingProperties import com.few.generator.event.GenSchedulingCompletedEvent +import com.few.generator.service.ContentsCommonGenerationService import com.few.generator.service.GenService -import com.few.generator.service.ProvisioningService -import com.few.generator.service.specifics.groupgen.GenGroupper -import com.few.generator.service.specifics.groupgen.GroupContentGenerator import com.few.generator.service.specifics.groupgen.GroupGenMetrics -import com.few.generator.service.specifics.groupgen.KeywordExtractor import com.google.gson.Gson import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.ApplicationEventPublisher @@ -21,24 +17,16 @@ import org.springframework.stereotype.Component class GlobalGroupGenSchedulingUseCase( applicationEventPublisher: ApplicationEventPublisher, genService: GenService, - provisioningService: ProvisioningService, - groupingProperties: GroupingProperties, @Qualifier(GSON_BEAN_NAME) gson: Gson, groupGenMetrics: GroupGenMetrics, - keywordExtractor: KeywordExtractor, - genGrouper: GenGroupper, - groupContentGenerator: GroupContentGenerator, + contentsCommonGenerationService: ContentsCommonGenerationService, ) : AbstractGroupGenSchedulingUseCase( applicationEventPublisher, genService, - provisioningService, - groupingProperties, gson, groupGenMetrics, - keywordExtractor, - genGrouper, - groupContentGenerator, + contentsCommonGenerationService, ) { override val region = Region.GLOBAL override val regionName = "GLOBAL" diff --git a/domain/generator/src/main/kotlin/com/few/generator/usecase/LocalGroupGenSchedulingUseCase.kt b/domain/generator/src/main/kotlin/com/few/generator/usecase/LocalGroupGenSchedulingUseCase.kt index bec39c217..646cf38e8 100644 --- a/domain/generator/src/main/kotlin/com/few/generator/usecase/LocalGroupGenSchedulingUseCase.kt +++ b/domain/generator/src/main/kotlin/com/few/generator/usecase/LocalGroupGenSchedulingUseCase.kt @@ -2,14 +2,10 @@ package com.few.generator.usecase import com.few.common.domain.Region import com.few.generator.config.GeneratorGsonConfig.Companion.GSON_BEAN_NAME -import com.few.generator.config.GroupingProperties import com.few.generator.event.GenSchedulingCompletedEvent +import com.few.generator.service.ContentsCommonGenerationService import com.few.generator.service.GenService -import com.few.generator.service.ProvisioningService -import com.few.generator.service.specifics.groupgen.GenGroupper -import com.few.generator.service.specifics.groupgen.GroupContentGenerator import com.few.generator.service.specifics.groupgen.GroupGenMetrics -import com.few.generator.service.specifics.groupgen.KeywordExtractor import com.google.gson.Gson import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.ApplicationEventPublisher @@ -21,24 +17,16 @@ import org.springframework.stereotype.Component class LocalGroupGenSchedulingUseCase( applicationEventPublisher: ApplicationEventPublisher, genService: GenService, - provisioningService: ProvisioningService, - groupingProperties: GroupingProperties, @Qualifier(GSON_BEAN_NAME) gson: Gson, groupGenMetrics: GroupGenMetrics, - keywordExtractor: KeywordExtractor, - genGrouper: GenGroupper, - groupContentGenerator: GroupContentGenerator, + contentsCommonGenerationService: ContentsCommonGenerationService, ) : AbstractGroupGenSchedulingUseCase( applicationEventPublisher, genService, - provisioningService, - groupingProperties, gson, groupGenMetrics, - keywordExtractor, - genGrouper, - groupContentGenerator, + contentsCommonGenerationService, ) { override val region = Region.LOCAL override val regionName = "LOCAL"