diff --git a/api/src/main/kotlin/com/few/api/config/ApiJpaConfig.kt b/api/src/main/kotlin/com/few/api/config/ApiJpaConfig.kt new file mode 100644 index 000000000..47707d8e3 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/config/ApiJpaConfig.kt @@ -0,0 +1,8 @@ +package com.few.api.config + +import org.springframework.context.annotation.Configuration +import repo.jpa.EnableJpaRepositories + +@Configuration +@EnableJpaRepositories(basePackages = [ApiConfig.BASE_PACKAGE]) +class ApiJpaConfig \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddArticleUseCase.kt index 1ed3eaa77..99ea255dd 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddArticleUseCase.kt @@ -23,7 +23,7 @@ import com.few.api.domain.problem.repo.command.InsertProblemsCommand import com.few.api.domain.problem.repo.support.Content import com.few.api.domain.problem.repo.support.Contents import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import storage.document.PutDocumentProvider import java.io.File import java.time.LocalDateTime @@ -41,7 +41,7 @@ class AddArticleUseCase( private val getUrlService: GetUrlService, private val adminArticleMainCardService: AdminArticleMainCardService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut { val writerRecord = memberDao.selectMemberByEmail(SelectMemberByEmailQuery(useCaseIn.writerEmail)) diff --git a/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddWorkbookUseCase.kt index 4700b7ae4..bc562661f 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/usecase/AddWorkbookUseCase.kt @@ -6,13 +6,13 @@ import com.few.api.domain.common.exception.InsertException import com.few.api.domain.workbook.repo.WorkbookDao import com.few.api.domain.workbook.repo.command.InsertWorkBookCommand import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class AddWorkbookUseCase( private val workbookDao: WorkbookDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: AddWorkbookUseCaseIn): AddWorkbookUseCaseOut { val workbookId = workbookDao.insertWorkBook( diff --git a/api/src/main/kotlin/com/few/api/domain/admin/usecase/ConvertContentUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/usecase/ConvertContentUseCase.kt index 9eda387df..74e25151f 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/usecase/ConvertContentUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/usecase/ConvertContentUseCase.kt @@ -10,7 +10,7 @@ import com.few.api.domain.admin.utils.ObjectPathGenerator import com.few.api.domain.common.exception.ExternalIntegrationException import com.few.api.domain.common.exception.InsertException import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import storage.document.PutDocumentProvider import java.io.File @@ -21,7 +21,7 @@ class ConvertContentUseCase( private val putDocumentService: PutDocumentProvider, private val getUrlService: GetUrlService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: ConvertContentUseCaseIn): ConvertContentUseCaseOut { val contentSource = useCaseIn.content diff --git a/api/src/main/kotlin/com/few/api/domain/admin/usecase/MapArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/usecase/MapArticleUseCase.kt index 1eda89f30..938d1ad4b 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/usecase/MapArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/usecase/MapArticleUseCase.kt @@ -6,14 +6,14 @@ import com.few.api.domain.admin.usecase.dto.MapArticleUseCaseIn import com.few.api.domain.workbook.repo.WorkbookDao import com.few.api.domain.workbook.repo.command.MapWorkBookToArticleCommand import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class MapArticleUseCase( private val workbookDao: WorkbookDao, private val adminArticleMainCardService: AdminArticleMainCardService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: MapArticleUseCaseIn) { workbookDao.mapWorkBookToArticle( MapWorkBookToArticleCommand( diff --git a/api/src/main/kotlin/com/few/api/domain/admin/usecase/PutImageUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/usecase/PutImageUseCase.kt index ba8e02e60..197b74aa0 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/usecase/PutImageUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/usecase/PutImageUseCase.kt @@ -12,7 +12,7 @@ import com.few.api.domain.common.exception.InsertException import com.sksamuel.scrimage.ImmutableImage import com.sksamuel.scrimage.webp.WebpWriter import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import storage.image.PutImageProvider import java.io.File @@ -22,7 +22,7 @@ class PutImageUseCase( private val putImageService: PutImageProvider, private val getUrlService: GetUrlService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: PutImageUseCaseIn): PutImageUseCaseOut { val imageSource = useCaseIn.source val suffix = imageSource.originalFilename?.substringAfterLast(".") ?: "jpg" diff --git a/api/src/main/kotlin/com/few/api/domain/article/event/handler/ArticleViewHisAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/article/event/handler/ArticleViewHisAsyncHandler.kt index f0b3a81cb..10ef91776 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/event/handler/ArticleViewHisAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/event/handler/ArticleViewHisAsyncHandler.kt @@ -9,7 +9,7 @@ import com.few.api.domain.common.vo.CategoryType import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class ArticleViewHisAsyncHandler( @@ -19,7 +19,7 @@ class ArticleViewHisAsyncHandler( private val log = KotlinLogging.logger {} @Async(value = DATABASE_ACCESS_POOL) - @Transactional + @DataSourceTransactional fun addArticleViewHis( articleId: Long, memberId: Long, diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt index c15994c13..7eace8e2b 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt @@ -12,7 +12,7 @@ import com.few.api.domain.article.usecase.dto.* import com.few.api.domain.common.exception.NotFoundException import com.few.api.domain.common.vo.CategoryType import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import java.util.* import kotlin.Comparator @@ -22,7 +22,7 @@ class BrowseArticlesUseCase( private val articleMainCardDao: ArticleMainCardDao, private val articleDao: ArticleDao, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: ReadArticlesUseCaseIn): ReadArticlesUseCaseOut { /** * 아티클 조회수 테이블에서 마지막 읽은 아티클 아이디, 카테고리를 기반으로 Offset(테이블 row 순위)을 구함 diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt index 09747d066..4a9d04491 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleByEmailUseCase.kt @@ -9,14 +9,14 @@ import com.few.api.domain.article.usecase.dto.ReadArticleByEmailUseCaseIn import com.few.api.domain.common.exception.NotFoundException import com.few.api.domain.common.vo.EmailLogEventType import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class ReadArticleByEmailUseCase( private val memberService: ArticleMemberService, private val articleLogService: ArticleLogService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: ReadArticleByEmailUseCaseIn) { val memberId = memberService.readMemberByEmail(ReadMemberByEmailDto(useCaseIn.destination[0])) diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt index 0c151e739..66e346976 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt @@ -15,7 +15,7 @@ import com.few.api.domain.common.exception.NotFoundException import com.few.api.domain.common.vo.CategoryType import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class ReadArticleUseCase( @@ -25,7 +25,7 @@ class ReadArticleUseCase( private val articleViewCountTxCase: ArticleViewCountTxCase, private val applicationEventPublisher: ApplicationEventPublisher, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: ReadArticleUseCaseIn): ReadArticleUseCaseOut { val articleRecord = articleDao.selectArticleRecord(SelectArticleRecordQuery(useCaseIn.articleId)) diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/transaction/ArticleViewCountTxCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/transaction/ArticleViewCountTxCase.kt index edc661de4..6befa332c 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/transaction/ArticleViewCountTxCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/transaction/ArticleViewCountTxCase.kt @@ -5,13 +5,13 @@ import com.few.api.domain.article.repo.command.ArticleViewCountCommand import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class ArticleViewCountTxCase( private val articleViewCountDao: ArticleViewCountDao, ) { - @Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW) + @DataSourceTransactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRES_NEW) fun browseArticleViewCount(articleId: Long): Long = (articleViewCountDao.selectArticleViewCount(ArticleViewCountCommand(articleId)) ?: 0L) + 1L } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/batch/article/ApiBatchSendArticleEmailService.kt b/api/src/main/kotlin/com/few/api/domain/batch/article/ApiBatchSendArticleEmailService.kt index 0a36e5515..620715c27 100644 --- a/api/src/main/kotlin/com/few/api/domain/batch/article/ApiBatchSendArticleEmailService.kt +++ b/api/src/main/kotlin/com/few/api/domain/batch/article/ApiBatchSendArticleEmailService.kt @@ -5,7 +5,7 @@ import com.few.api.domain.batch.article.reader.WorkBookSubscriberReader import com.few.api.domain.batch.article.writer.WorkBookSubscriberWriter import com.few.api.domain.batch.log.ApiBatchCallExecutionService import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Service class ApiBatchSendArticleEmailService( @@ -14,7 +14,7 @@ class ApiBatchSendArticleEmailService( private val batchCallExecutionService: ApiBatchCallExecutionService, private val objectMapper: ObjectMapper, ) { - @Transactional + @DataSourceTransactional fun execute() { val startTime = System.currentTimeMillis() workBookSubscriberReader.execute().let { item -> diff --git a/api/src/main/kotlin/com/few/api/domain/batch/article/reader/WorkBookSubscriberReader.kt b/api/src/main/kotlin/com/few/api/domain/batch/article/reader/WorkBookSubscriberReader.kt index 233bf9c17..edb38480f 100644 --- a/api/src/main/kotlin/com/few/api/domain/batch/article/reader/WorkBookSubscriberReader.kt +++ b/api/src/main/kotlin/com/few/api/domain/batch/article/reader/WorkBookSubscriberReader.kt @@ -8,7 +8,7 @@ import org.jooq.Condition import org.jooq.DSLContext import org.jooq.TableField import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalTime @@ -19,7 +19,7 @@ class WorkBookSubscriberReader( private val dslContext: DSLContext, ) { /** 구독 테이블에서 학습지를 구독하고 있는 회원의 정보를 조회한다.*/ - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(): List { val time = LocalTime.now(ZoneId.of("Asia/Seoul")).hour.let { LocalTime.of(it, 0, 0) } val date = LocalDate.now(ZoneId.of("Asia/Seoul")) diff --git a/api/src/main/kotlin/com/few/api/domain/batch/article/writer/WorkBookSubscriberWriter.kt b/api/src/main/kotlin/com/few/api/domain/batch/article/writer/WorkBookSubscriberWriter.kt index e95e6e66d..8a5832502 100644 --- a/api/src/main/kotlin/com/few/api/domain/batch/article/writer/WorkBookSubscriberWriter.kt +++ b/api/src/main/kotlin/com/few/api/domain/batch/article/writer/WorkBookSubscriberWriter.kt @@ -19,7 +19,7 @@ import org.jooq.DSLContext import org.jooq.InsertSetMoreStep import org.jooq.UpdateConditionStep import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import java.time.LocalDate import java.time.LocalDateTime @@ -35,7 +35,7 @@ class WorkBookSubscriberWriter( /** * 구독자들에게 이메일을 전송하고 진행률을 업데이트한다. */ - @Transactional + @DataSourceTransactional fun execute(items: List): Map { val memberIds = items.toMemberIds() val targetWorkBookIds = items.toTargetWorkBookIds() diff --git a/api/src/main/kotlin/com/few/api/domain/log/usecase/AddApiLogUseCase.kt b/api/src/main/kotlin/com/few/api/domain/log/usecase/AddApiLogUseCase.kt index c3f22f9c0..1a727aff5 100644 --- a/api/src/main/kotlin/com/few/api/domain/log/usecase/AddApiLogUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/log/usecase/AddApiLogUseCase.kt @@ -4,13 +4,13 @@ import com.few.api.domain.log.dto.AddApiLogUseCaseIn import com.few.api.domain.log.repo.LogIfoDao import com.few.api.domain.log.repo.command.InsertLogCommand import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class AddApiLogUseCase( private val logIfoDao: LogIfoDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: AddApiLogUseCaseIn) { logIfoDao.insertLogIfo(InsertLogCommand(useCaseIn.history)) } diff --git a/api/src/main/kotlin/com/few/api/domain/log/usecase/AddEmailLogUseCase.kt b/api/src/main/kotlin/com/few/api/domain/log/usecase/AddEmailLogUseCase.kt index 73ec14f6b..e94244077 100644 --- a/api/src/main/kotlin/com/few/api/domain/log/usecase/AddEmailLogUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/log/usecase/AddEmailLogUseCase.kt @@ -9,14 +9,14 @@ import com.few.api.domain.log.repo.query.SelectEventByMessageIdAndEventTypeQuery import com.few.api.domain.member.repo.MemberDao import com.few.api.domain.member.repo.query.SelectMemberByEmailQuery import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class AddEmailLogUseCase( private val memberDao: MemberDao, private val sendArticleEventHistoryDao: SendArticleEventHistoryDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: AddEmailLogUseCaseIn) { val (memberId, _, _, _) = memberDao.selectMemberByEmail( diff --git a/api/src/main/kotlin/com/few/api/domain/member/service/MemberSubscriptionService.kt b/api/src/main/kotlin/com/few/api/domain/member/service/MemberSubscriptionService.kt index 108fcd645..c302e5d53 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/service/MemberSubscriptionService.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/service/MemberSubscriptionService.kt @@ -4,13 +4,13 @@ import com.few.api.domain.member.service.dto.DeleteSubscriptionDto import com.few.api.domain.subscription.repo.SubscriptionDao import com.few.api.domain.subscription.repo.command.UpdateDeletedAtInAllSubscriptionCommand import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Service class MemberSubscriptionService( private val subscriptionDao: SubscriptionDao, ) { - @Transactional + @DataSourceTransactional fun deleteSubscription(dto: DeleteSubscriptionDto) { subscriptionDao.updateDeletedAtInAllSubscription( UpdateDeletedAtInAllSubscriptionCommand( diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt index b7199fa03..925807162 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt @@ -7,14 +7,14 @@ import com.few.api.domain.member.service.dto.DeleteSubscriptionDto import com.few.api.domain.member.usecase.dto.DeleteMemberUseCaseIn import com.few.api.domain.member.usecase.dto.DeleteMemberUseCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class DeleteMemberUseCase( private val memberDao: MemberDao, private val memberSubscriptionService: MemberSubscriptionService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: DeleteMemberUseCaseIn): DeleteMemberUseCaseOut { memberDao.deleteMember( DeleteMemberCommand( diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt index 66eff6476..7e700bd96 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt @@ -10,7 +10,7 @@ import com.few.api.domain.member.usecase.dto.SaveMemberUseCaseIn import com.few.api.domain.member.usecase.dto.SaveMemberUseCaseOut import com.few.api.domain.member.usecase.transaction.SaveMemberTxCase import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import security.encryptor.IdEncryptor import java.net.URL @@ -21,7 +21,7 @@ class SaveMemberUseCase( private val idEncryption: IdEncryptor, private val saveMemberTxCase: SaveMemberTxCase, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: SaveMemberUseCaseIn): SaveMemberUseCaseOut { /** email을 통해 가입 이력이 있는지 확인 */ val (headComment, subComment, memberId) = diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt index 26d05652f..71b241d01 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt @@ -8,7 +8,7 @@ import com.few.api.domain.member.repo.command.UpdateMemberTypeCommand import com.few.api.domain.member.usecase.dto.TokenUseCaseIn import com.few.api.domain.member.usecase.dto.TokenUseCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import security.Roles import security.TokenGenerator import security.TokenResolver @@ -22,7 +22,7 @@ class TokenUseCase( private val memberDao: MemberDao, private val idEncryption: IdEncryptor, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: TokenUseCaseIn): TokenUseCaseOut { var isLogin = true diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/transaction/SaveMemberTxCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/transaction/SaveMemberTxCase.kt index 0f58c83b0..07de27ef8 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/transaction/SaveMemberTxCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/transaction/SaveMemberTxCase.kt @@ -9,7 +9,7 @@ import com.few.api.domain.member.repo.record.MemberIdAndIsDeletedRecord import com.few.api.domain.member.usecase.dto.SaveMemberTxCaseIn import com.few.api.domain.member.usecase.dto.SaveMemberTxCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class SaveMemberTxCase( @@ -22,7 +22,7 @@ class SaveMemberTxCase( private const val SIGNUP_SUB_COMMENT = "가입하신 이메일 주소를 확인해주세요." } - @Transactional + @DataSourceTransactional fun execute(dto: SaveMemberTxCaseIn): SaveMemberTxCaseOut = dto.record?.let { signUpBeforeMember -> signUpBeforeMember.takeIf { it.isDeleted }?.let { diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt index 036dfb420..1c72fd4b0 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt @@ -6,13 +6,13 @@ import com.few.api.domain.problem.repo.query.SelectProblemsByArticleIdQuery import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseIn import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class BrowseProblemsUseCase( private val problemDao: ProblemDao, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: BrowseProblemsUseCaseIn): BrowseProblemsUseCaseOut { problemDao .selectProblemsByArticleId(SelectProblemsByArticleIdQuery(useCaseIn.articleId)) diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt index 0d9eac243..e6a8554e2 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt @@ -12,7 +12,7 @@ import com.few.api.domain.problem.service.dto.BrowseWorkbookIdAndProgressInDto import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseOut import com.few.api.domain.problem.usecase.dto.BrowseUndoneProblemsUseCaseIn import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class BrowseUndoneProblemsUseCase( @@ -21,7 +21,7 @@ class BrowseUndoneProblemsUseCase( private val problemArticleService: ProblemArticleService, private val submitHistoryDao: SubmitHistoryDao, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: BrowseUndoneProblemsUseCaseIn): BrowseProblemsUseCaseOut { /** * 유저가 구독한 워크북들에 속한 아티클 개수를 조회함 diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt index e7c19da70..0456bfec2 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt @@ -9,14 +9,14 @@ import com.few.api.domain.problem.repo.query.SelectProblemAnswerQuery import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseIn import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class CheckProblemUseCase( private val problemDao: ProblemDao, private val submitHistoryDao: SubmitHistoryDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: CheckProblemUseCaseIn): CheckProblemUseCaseOut { val memberId = useCaseIn.memberId val problemId = useCaseIn.problemId diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt index f10b122da..4fec21364 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt @@ -8,14 +8,14 @@ import com.few.api.domain.problem.usecase.dto.ReadProblemContentsUseCaseOutDetai import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseIn import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseOut import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class ReadProblemUseCase( private val problemDao: ProblemDao, private val contentsJsonMapper: ContentsJsonMapper, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: ReadProblemUseCaseIn): ReadProblemUseCaseOut { val problemId = useCaseIn.problemId diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/event/handler/SendWorkbookArticleAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/subscription/event/handler/SendWorkbookArticleAsyncHandler.kt index 6bdbb3407..8061fe8ac 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/event/handler/SendWorkbookArticleAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/event/handler/SendWorkbookArticleAsyncHandler.kt @@ -16,7 +16,7 @@ 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 repo.jooq.DataSourceTransactional import java.time.LocalDate @Component @@ -32,7 +32,7 @@ class SendWorkbookArticleAsyncHandler( @Async(value = DATABASE_ACCESS_POOL) @ApiLockFor(identifier = ApiLockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID) - @Transactional(propagation = Propagation.REQUIRES_NEW) + @DataSourceTransactional(propagation = Propagation.REQUIRES_NEW) fun sendWorkbookArticle( memberId: Long, workbookId: Long, diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt index e209ea526..847625584 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt @@ -15,7 +15,7 @@ import com.few.api.domain.subscription.service.dto.ReadAllWorkbookTitleInDto import com.few.api.domain.subscription.service.dto.ReadArticleIdByWorkbookIdAndDayDto import com.few.api.domain.subscription.usecase.dto.* import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional import java.lang.IllegalStateException @Suppress("ktlint:standard:class-naming") @@ -51,7 +51,7 @@ class BrowseSubscribeWorkbooksUseCase( private val subscriptionWorkbookService: SubscriptionWorkbookService, private val objectMapper: ObjectMapper, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: BrowseSubscribeWorkbooksUseCaseIn): BrowseSubscribeWorkbooksUseCaseOut { val strategy = when (useCaseIn.view) { diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt index 3f621605f..882ec542b 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt @@ -16,7 +16,7 @@ import com.few.api.domain.subscription.usecase.model.WorkbookSubscriptionHistory import com.few.api.domain.subscription.usecase.model.WorkbookSubscriptionStatus import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class SubscribeWorkbookUseCase( @@ -24,7 +24,7 @@ class SubscribeWorkbookUseCase( private val applicationEventPublisher: ApplicationEventPublisher, ) { @ApiLockFor(ApiLockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID) - @Transactional + @DataSourceTransactional fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) { val subTargetWorkbookId = useCaseIn.workbookId val memberId = useCaseIn.memberId diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt index 37a2bd523..7975acfff 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt @@ -4,13 +4,13 @@ import com.few.api.domain.subscription.repo.SubscriptionDao import com.few.api.domain.subscription.repo.command.UpdateDeletedAtInAllSubscriptionCommand import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class UnsubscribeAllUseCase( private val subscriptionDao: SubscriptionDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: UnsubscribeAllUseCaseIn) { // TODO: request sending email var opinion = useCaseIn.opinion diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt index 16c95ea6f..d1bc87e16 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt @@ -4,14 +4,14 @@ import com.few.api.domain.subscription.repo.SubscriptionDao import com.few.api.domain.subscription.repo.command.UpdateDeletedAtInWorkbookSubscriptionCommand import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class UnsubscribeWorkbookUseCase( private val subscriptionDao: SubscriptionDao, ) { // todo add test - @Transactional + @DataSourceTransactional fun execute(useCaseIn: UnsubscribeWorkbookUseCaseIn) { // TODO: request sending email var opinion = useCaseIn.opinion diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt index f6e1d1449..0954b3159 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt @@ -4,13 +4,13 @@ import com.few.api.domain.subscription.repo.SubscriptionDao import com.few.api.domain.subscription.repo.command.BulkUpdateSubscriptionSendDayCommand import com.few.api.domain.subscription.usecase.dto.UpdateSubscriptionDayUseCaseIn import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class UpdateSubscriptionDayUseCase( private val subscriptionDao: SubscriptionDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: UpdateSubscriptionDayUseCaseIn) { /** * workbookId기 없으면, memberId로 구독중인 모든 workbookId를 가져와서 해당하는 모든 workbookId의 구독요일을 변경한다. diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt index 931531b1f..a741bab81 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt @@ -4,13 +4,13 @@ import com.few.api.domain.subscription.repo.SubscriptionDao import com.few.api.domain.subscription.repo.command.BulkUpdateSubscriptionSendTimeCommand import com.few.api.domain.subscription.usecase.dto.UpdateSubscriptionTimeUseCaseIn import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Component class UpdateSubscriptionTimeUseCase( private val subscriptionDao: SubscriptionDao, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: UpdateSubscriptionTimeUseCaseIn) { /** * workbookId기 없으면, memberId로 구독중인 모든 workbookId를 가져와서 해당하는 모든 workbookId의 구독요일을 변경한다. diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt index 2ea01a954..b032be2f8 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt @@ -15,7 +15,7 @@ import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleUseCaseIn import com.few.api.domain.workbook.article.dto.WriterDetail import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional @Service class ReadWorkBookArticleUseCase( @@ -25,7 +25,7 @@ class ReadWorkBookArticleUseCase( private val articleViewCountTxCase: ArticleViewCountTxCase, private val applicationEventPublisher: ApplicationEventPublisher, ) { - @Transactional(readOnly = true) + @DataSourceTransactional(readOnly = true) fun execute(useCaseIn: ReadWorkBookArticleUseCaseIn): ReadWorkBookArticleOut { val articleRecord = articleDao.selectWorkBookArticleRecord( diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt index c1d0c1539..6c8f16686 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt @@ -17,7 +17,7 @@ import com.few.api.domain.workbook.usecase.dto.WriterDetail import com.few.api.domain.workbook.usecase.model.* import com.few.api.domain.workbook.usecase.model.order.* import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional +import repo.jooq.DataSourceTransactional enum class WorkBookOrderStrategy { BASIC, @@ -43,7 +43,7 @@ class BrowseWorkbooksUseCase( private val workbookMemberService: WorkbookMemberService, private val workbookSubscribeService: WorkbookSubscribeService, ) { - @Transactional + @DataSourceTransactional fun execute(useCaseIn: BrowseWorkbooksUseCaseIn): BrowseWorkbooksUseCaseOut { val workbookRecords = workbookDao.browseWorkBookWithSubscriptionCount( diff --git a/api/src/test/resources/application-test.yml b/api/src/test/resources/application-test.yml index bbd73bb80..ac826e906 100644 --- a/api/src/test/resources/application-test.yml +++ b/api/src/test/resources/application-test.yml @@ -17,6 +17,12 @@ spring: sql-migration-suffixes: sql baseline-on-migrate: true baseline-version: 0 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect mail: protocol: smtp host: smtp.gmail.com diff --git a/build.gradle.kts b/build.gradle.kts index 6181afa1d..29e480611 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -126,6 +126,9 @@ subprojects { * jpa meta-annotations not automatically opened through the default settings of the plugin.spring */ allOpen { + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") } dependencyManagement { @@ -253,10 +256,32 @@ subprojects { val dataMigrationDir = "$root/data/$flyWayResourceDir" File(dataMigrationDir).walkTopDown().forEach { if (it.isFile) { - it.copyTo( - File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), - true, - ) + // Migration is executed in Api module. So, copy the migration file to the Api module. + if (project.name == "api") { + it.copyTo( + File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), + true, + ) + println("Copy ${it.name} to ${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}") + // If domain module, Copy domain necessary migration files. + } else if (project.name.contains("domain")) { + // Copy all V1 migration files. + if (it.name.startsWith("V1.")) { + it.copyTo( + File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), + true, + ) + // Copy domain specific migration files. + } else if (it.name.startsWith("V2.")) { + if (it.name.contains("__${project.name}__")) { + it.copyTo( + File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), + true, + ) + } + } + println("Copy ${it.name} to ${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}") + } } } } @@ -334,14 +359,13 @@ subprojects { defaultTasks("bootRun") } -/** do all copy data migration */ -tasks.register("copyDataMigrationAll") { - dependsOn(":api:copyDataMigration") -} - /** do all jooq codegen */ tasks.register("jooqCodegenAll") { - dependsOn("copyDataMigrationAll") + /** copy data migration */ + subprojects.forEach { + dependsOn(it.tasks.named("copyDataMigration")) + } + /** jooq codegen */ dependsOn(":api:jooqCodegen") } diff --git a/repo/build.gradle.kts b/repo/build.gradle.kts index d49edaef2..78971c205 100644 --- a/repo/build.gradle.kts +++ b/repo/build.gradle.kts @@ -16,6 +16,9 @@ dependencies { /** jooq */ api("org.springframework.boot:spring-boot-starter-jooq") + /** jpa */ + api("org.springframework.boot:spring-boot-starter-data-jpa") + /** flyway */ implementation("org.flywaydb:flyway-core:${DependencyVersion.FLYWAY}") implementation("org.flywaydb:flyway-mysql") diff --git a/repo/src/main/kotlin/repo/config/DataSourceConfig.kt b/repo/src/main/kotlin/repo/config/DataSourceConfig.kt index fba982723..1408f7871 100644 --- a/repo/src/main/kotlin/repo/config/DataSourceConfig.kt +++ b/repo/src/main/kotlin/repo/config/DataSourceConfig.kt @@ -5,21 +5,15 @@ import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.jdbc.DataSourceBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.jdbc.datasource.DataSourceTransactionManager -import org.springframework.transaction.PlatformTransactionManager import javax.sql.DataSource @Configuration class DataSourceConfig { companion object { - const val API_DATASOURCE = RepoConfig.BEAN_NAME_PREFIX + "DataSource" - const val API_TX = RepoConfig.BEAN_NAME_PREFIX + "TransactionManager" + const val DATASOURCE = RepoConfig.BEAN_NAME_PREFIX + "DataSource" } - @Bean(name = [API_DATASOURCE]) + @Bean(name = [DATASOURCE]) @ConfigurationProperties(prefix = "spring.datasource.hikari") - fun apiDataSource(): DataSource = DataSourceBuilder.create().type(HikariDataSource::class.java).build() - - @Bean(name = [API_TX]) - fun apiTransactionManager(): PlatformTransactionManager = DataSourceTransactionManager(apiDataSource()) + fun dataSource(): DataSource = DataSourceBuilder.create().type(HikariDataSource::class.java).build() } \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/config/FlywayConfig.kt b/repo/src/main/kotlin/repo/config/FlywayConfig.kt index aaa8a1d88..67a4cb32e 100644 --- a/repo/src/main/kotlin/repo/config/FlywayConfig.kt +++ b/repo/src/main/kotlin/repo/config/FlywayConfig.kt @@ -42,7 +42,7 @@ class FlywayConfig { @Bean(name = [FLYWAY_CONFIGURATION]) fun configuration( - @Qualifier(DataSourceConfig.API_DATASOURCE) dataSource: DataSource?, + @Qualifier(DataSourceConfig.DATASOURCE) dataSource: DataSource, ): org.flywaydb.core.api.configuration.Configuration { val configuration = FluentConfiguration() configuration.dataSource(dataSource) diff --git a/repo/src/main/kotlin/repo/config/JooqConfig.kt b/repo/src/main/kotlin/repo/config/JooqConfig.kt index 8ad4178d2..126171efe 100644 --- a/repo/src/main/kotlin/repo/config/JooqConfig.kt +++ b/repo/src/main/kotlin/repo/config/JooqConfig.kt @@ -4,18 +4,26 @@ import org.jooq.SQLDialect import org.jooq.impl.DataSourceConnectionProvider import org.jooq.impl.DefaultConfiguration import org.jooq.impl.DefaultDSLContext +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.jooq.SpringTransactionProvider import org.springframework.context.ApplicationEventPublisher import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator +import org.springframework.transaction.PlatformTransactionManager +import repo.config.DataSourceConfig.Companion.DATASOURCE +import repo.config.TxConfig.Companion.DATASOURCE_TX import repo.flyway.support.ExceptionTranslator import repo.flyway.support.NativeSQLLogger import repo.flyway.support.PerformanceListener import javax.sql.DataSource @Configuration +@Import(DataSourceConfig::class, TxConfig::class) class JooqConfig( - private val dataSource: DataSource, + @Qualifier(DATASOURCE) private val dataSource: DataSource, + @Qualifier(DATASOURCE_TX) private val txManager: PlatformTransactionManager, private val applicationEventPublisher: ApplicationEventPublisher, ) { companion object { @@ -40,4 +48,7 @@ class JooqConfig( @Bean(name = [JOOQ_CONNECTION_PROVIDER]) fun connectionProvider(): DataSourceConnectionProvider = DataSourceConnectionProvider(dataSource) + + @Bean + fun transactionProvider(): SpringTransactionProvider = SpringTransactionProvider(txManager) } \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/config/JpaConfig.kt b/repo/src/main/kotlin/repo/config/JpaConfig.kt new file mode 100644 index 000000000..49d67ac87 --- /dev/null +++ b/repo/src/main/kotlin/repo/config/JpaConfig.kt @@ -0,0 +1,56 @@ +package repo.config + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration +import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties +import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter +import javax.sql.DataSource + +@Configuration +@EnableAutoConfiguration( + exclude = [ + DataSourceAutoConfiguration::class, + DataSourceTransactionManagerAutoConfiguration::class, + HibernateJpaAutoConfiguration::class, + ], +) +@EnableJpaAuditing +class JpaConfig { + companion object { + const val ENTITY_UNIT = "few" + const val ENTITY_PACKAGE = "com.few" + const val JPA_EMF = RepoConfig.BEAN_NAME_PREFIX + "JpaEntityManagerFactory" + } + + @Bean(name = [JPA_EMF]) + fun entityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean { + val jpaPropertyMap = jpaProperties().properties + val hibernatePropertyMap = + hibernateProperties().determineHibernateProperties(jpaPropertyMap, HibernateSettings()) + + return EntityManagerFactoryBuilder(HibernateJpaVendorAdapter(), jpaPropertyMap, null) + .dataSource(dataSource) + .properties(hibernatePropertyMap) + .persistenceUnit(ENTITY_UNIT) + .packages(ENTITY_PACKAGE) + .build() + } + + @Bean + @ConfigurationProperties(prefix = "spring.jpa") + fun jpaProperties(): JpaProperties = JpaProperties() + + @Bean + @ConfigurationProperties(prefix = "spring.jpa.hibernate") + fun hibernateProperties(): HibernateProperties = HibernateProperties() +} \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/config/TxConfig.kt b/repo/src/main/kotlin/repo/config/TxConfig.kt new file mode 100644 index 000000000..b89f461b2 --- /dev/null +++ b/repo/src/main/kotlin/repo/config/TxConfig.kt @@ -0,0 +1,36 @@ +package repo.config + +import jakarta.persistence.EntityManagerFactory +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.jdbc.datasource.DataSourceTransactionManager +import org.springframework.orm.jpa.JpaTransactionManager +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.TransactionManager +import org.springframework.transaction.annotation.EnableTransactionManagement +import org.springframework.transaction.annotation.TransactionManagementConfigurer +import repo.config.DataSourceConfig.Companion.DATASOURCE +import javax.sql.DataSource + +@Import(JpaConfig::class, DataSourceConfig::class) +@Configuration +@EnableTransactionManagement +class TxConfig( + private val emf: EntityManagerFactory, + @Qualifier(DATASOURCE) private val dataSource: DataSource, +) : TransactionManagementConfigurer { + companion object { + const val JPA_TX = RepoConfig.BEAN_NAME_PREFIX + "JpaTransactionManager" + const val DATASOURCE_TX = RepoConfig.BEAN_NAME_PREFIX + "DataSourceTransactionManager" + } + + @Bean(name = [JPA_TX]) + fun jpaTransactionManager(): PlatformTransactionManager = JpaTransactionManager(emf) + + @Bean(name = [DATASOURCE_TX]) + fun dataSourceTransactionManager(): PlatformTransactionManager = DataSourceTransactionManager(dataSource) + + override fun annotationDrivenTransactionManager(): TransactionManager = jpaTransactionManager() +} \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/jooq/DataSourceTransactional.kt b/repo/src/main/kotlin/repo/jooq/DataSourceTransactional.kt new file mode 100644 index 000000000..497c3e5c8 --- /dev/null +++ b/repo/src/main/kotlin/repo/jooq/DataSourceTransactional.kt @@ -0,0 +1,38 @@ +package repo.jooq + +import org.springframework.core.annotation.AliasFor +import org.springframework.transaction.annotation.Isolation +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import repo.config.TxConfig.Companion.DATASOURCE_TX +import kotlin.reflect.KClass + +/** + * DataSource 트랜잭션을 처리하는 어노테이션 + * transactionManager는 DataSourceConfig에서 설정한 값을 기본으로 사용한다. + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Transactional(transactionManager = DATASOURCE_TX) +annotation class DataSourceTransactional( + @get:AliasFor(annotation = Transactional::class, attribute = "label") + val label: Array = [], + @get:AliasFor(annotation = Transactional::class, attribute = "propagation") + val propagation: Propagation = Propagation.REQUIRED, + @get:AliasFor(annotation = Transactional::class, attribute = "isolation") + val isolation: Isolation = Isolation.DEFAULT, + @get:AliasFor(annotation = Transactional::class, attribute = "timeout") + val timeout: Int = -1, + @get:AliasFor(annotation = Transactional::class, attribute = "timeoutString") + val timeoutString: String = "", + @get:AliasFor(annotation = Transactional::class, attribute = "readOnly") + val readOnly: Boolean = false, + @get:AliasFor(annotation = Transactional::class, attribute = "rollbackFor") + val rollbackFor: Array> = [], + @get:AliasFor(annotation = Transactional::class, attribute = "rollbackForClassName") + val rollbackForClassName: Array = [], + @get:AliasFor(annotation = Transactional::class, attribute = "noRollbackFor") + val noRollbackFor: Array> = [], + @get:AliasFor(annotation = Transactional::class, attribute = "noRollbackForClassName") + val noRollbackForClassName: Array = [], +) \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/jpa/BaseEntity.kt b/repo/src/main/kotlin/repo/jpa/BaseEntity.kt new file mode 100644 index 000000000..ffa02f6ca --- /dev/null +++ b/repo/src/main/kotlin/repo/jpa/BaseEntity.kt @@ -0,0 +1,29 @@ +package repo.jpa + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.LocalDateTime + +@MappedSuperclass +@JsonIgnoreProperties(value = ["createdAt, modifiedAt"], allowGetters = true) +@EntityListeners(AuditingEntityListener::class) +abstract class BaseEntity { + /** 생성일 */ + @CreatedDate + @Column(name = "created_at", columnDefinition = "datetime default CURRENT_TIMESTAMP") + @EntityTimeJsonFormat + var createdAt: LocalDateTime = LocalDateTime.now() + + /** 수정일 */ + @LastModifiedDate + @Column(name = "modified_at", columnDefinition = "datetime default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + @EntityTimeJsonFormat + var modifiedAt: LocalDateTime = LocalDateTime.now() + + /** 삭제 여부 */ + @Column(name = "deleted", columnDefinition = "boolean default false") + var deleted: Boolean = false +} \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/jpa/EnableJpaRepositories.kt b/repo/src/main/kotlin/repo/jpa/EnableJpaRepositories.kt new file mode 100644 index 000000000..61b919f50 --- /dev/null +++ b/repo/src/main/kotlin/repo/jpa/EnableJpaRepositories.kt @@ -0,0 +1,52 @@ +package repo.jpa + +import org.springframework.context.annotation.ComponentScan +import org.springframework.core.annotation.AliasFor +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean +import org.springframework.data.repository.config.BootstrapMode +import org.springframework.data.repository.config.DefaultRepositoryBaseClass +import org.springframework.data.repository.query.QueryLookupStrategy +import repo.config.JpaConfig +import repo.config.TxConfig +import kotlin.reflect.KClass + +/** + * org.springframework.data.jpa.repository.config.EnableJpaRepositories를 대체하는 애노테이션 + * transactionManagerRef와 entityManagerFactoryRef는 TxConfig와 JpaConfig에서 설정한 값을 기본으로 사용한다. + * @see EnableJpaRepositories + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@EnableJpaRepositories( + transactionManagerRef = TxConfig.JPA_TX, + entityManagerFactoryRef = JpaConfig.JPA_EMF, +) +annotation class EnableJpaRepositories( + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "value") + val value: Array = [], + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "basePackages") + val basePackages: Array = [], + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "includeFilters") + val includeFilters: Array = [], + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "excludeFilters") + val excludeFilters: Array = [], + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "repositoryImplementationPostfix") + val repositoryImplementationPostfix: String = "Impl", + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "namedQueriesLocation") + val namedQueriesLocation: String = "", + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "queryLookupStrategy") + val queryLookupStrategy: QueryLookupStrategy.Key = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "repositoryFactoryBeanClass") + val repositoryFactoryBeanClass: KClass<*> = JpaRepositoryFactoryBean::class, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "repositoryBaseClass") + val repositoryBaseClass: KClass<*> = DefaultRepositoryBaseClass::class, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "considerNestedRepositories") + val considerNestedRepositories: Boolean = false, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "enableDefaultTransactions") + val enableDefaultTransactions: Boolean = true, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "bootstrapMode") + val bootstrapMode: BootstrapMode = BootstrapMode.DEFAULT, + @get:AliasFor(annotation = EnableJpaRepositories::class, attribute = "escapeCharacter") + val escapeCharacter: Char = '\\', +) \ No newline at end of file diff --git a/repo/src/main/kotlin/repo/jpa/EntityTimeJsonFormat.kt b/repo/src/main/kotlin/repo/jpa/EntityTimeJsonFormat.kt new file mode 100644 index 000000000..b029ff1fb --- /dev/null +++ b/repo/src/main/kotlin/repo/jpa/EntityTimeJsonFormat.kt @@ -0,0 +1,20 @@ +package repo.jpa + +import com.fasterxml.jackson.annotation.JsonFormat + +@MustBeDocumented +@Target( + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.FIELD, + AnnotationTarget.FUNCTION, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CLASS, +) +@Retention(AnnotationRetention.RUNTIME) +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = EntityTimeJsonFormat.TIME_FORMAT, timezone = EntityTimeJsonFormat.TIME_ZONE) +annotation class EntityTimeJsonFormat { + companion object { + const val TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + const val TIME_ZONE = "Asia/Seoul" + } +} \ No newline at end of file diff --git a/repo/src/main/resources/application-api-repo-local.yml b/repo/src/main/resources/application-api-repo-local.yml index 14bd2167f..e4ad0cd5d 100644 --- a/repo/src/main/resources/application-api-repo-local.yml +++ b/repo/src/main/resources/application-api-repo-local.yml @@ -17,3 +17,9 @@ spring: sql-migration-suffixes: sql baseline-on-migrate: true baseline-version: 0 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect diff --git a/repo/src/main/resources/application-api-repo-prd.yml b/repo/src/main/resources/application-api-repo-prd.yml index e22587a0c..c5b803c07 100644 --- a/repo/src/main/resources/application-api-repo-prd.yml +++ b/repo/src/main/resources/application-api-repo-prd.yml @@ -17,3 +17,9 @@ spring: sql-migration-suffixes: sql baseline-on-migrate: true baseline-version: 0 + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect diff --git a/web/src/main/kotlin/web/security/config/WebSecurityConfig.kt b/web/src/main/kotlin/web/security/config/WebSecurityConfig.kt index 507bb3b41..0d80e87c2 100644 --- a/web/src/main/kotlin/web/security/config/WebSecurityConfig.kt +++ b/web/src/main/kotlin/web/security/config/WebSecurityConfig.kt @@ -53,8 +53,9 @@ class WebSecurityConfig { } @Bean(name = [WEB_SECURITY_CONFIGURER]) - fun webSecurityConfigurer(userArgumentHandlerMethodArgumentResolver: HandlerMethodArgumentResolver): WebMvcConfigurer = - WebSecurityConfigurer(userArgumentHandlerMethodArgumentResolver) + fun webSecurityConfigurer( + @Qualifier(USER_ARGUMENT_HANDLER_METHOD_ARGUMENT_RESOLVER) userArgumentHandlerMethodArgumentResolver: HandlerMethodArgumentResolver, + ): WebMvcConfigurer = WebSecurityConfigurer(userArgumentHandlerMethodArgumentResolver) @Profile("local") @Bean(name = ["local$SECURITY_FILTER_CHAIN"])