From 93f872fb8d559f39a21899a791d28baf38bb9c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=ED=83=9C=EC=A7=84?= <140797244+taejinn@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:17:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=A0=A5=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/keyword/service/KeywordService.java | 3 ++- .../keyword/service/KeywordServiceTest.java | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java b/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java index b8e79e475..a87498e7c 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/service/KeywordService.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.domain.community.article.exception.ArticleNotFoundException; @@ -206,7 +207,7 @@ public void fetchTopKeywordsFromLastWeek() { } } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public void createNotifiedArticleStatus(Integer userId, Integer articleId) { userNotificationStatusRepository.upsertLastNotifiedArticleId(userId, articleId); } diff --git a/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java b/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java index 6ff874eb0..90edc4c71 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/community/keyword/service/KeywordServiceTest.java @@ -1,11 +1,13 @@ package in.koreatech.koin.unit.domain.community.keyword.service; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -16,6 +18,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.common.event.ArticleKeywordEvent; import in.koreatech.koin.domain.community.article.model.Article; @@ -107,4 +111,14 @@ void createNotifiedArticleStatus_usesAtomicUpsert() { verify(userNotificationStatusRepository).upsertLastNotifiedArticleId(1, 100); } + + @Test + @DisplayName("발송 이력 저장은 항상 새로운 트랜잭션에서 수행한다.") + void createNotifiedArticleStatus_startsNewTransaction() throws NoSuchMethodException { + Method method = KeywordService.class.getMethod("createNotifiedArticleStatus", Integer.class, Integer.class); + Transactional transactional = method.getAnnotation(Transactional.class); + + assertThat(transactional).isNotNull(); + assertThat(transactional.propagation()).isEqualTo(Propagation.REQUIRES_NEW); + } } From 9f3f04a0f1342188603a23e6a36539be2f7a4443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=ED=83=9C=EC=A7=84?= <140797244+taejinn@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:54:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=A0=A5=20upsert=20audit=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/UserNotificationStatusRepository.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/UserNotificationStatusRepository.java b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/UserNotificationStatusRepository.java index 9ab80b0e6..1981ab363 100644 --- a/src/main/java/in/koreatech/koin/domain/community/keyword/repository/UserNotificationStatusRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/keyword/repository/UserNotificationStatusRepository.java @@ -32,9 +32,11 @@ List findUserIdsByNotifiedArticleIdAndUserIdIn( @Modifying(flushAutomatically = true, clearAutomatically = true) @Query(value = """ - INSERT INTO user_notification_status (user_id, last_notified_article_id) - VALUES (:userId, :notifiedArticleId) - ON DUPLICATE KEY UPDATE last_notified_article_id = :notifiedArticleId + INSERT INTO user_notification_status (user_id, last_notified_article_id, created_at, updated_at) + VALUES (:userId, :notifiedArticleId, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON DUPLICATE KEY UPDATE + last_notified_article_id = :notifiedArticleId, + updated_at = CURRENT_TIMESTAMP """, nativeQuery = true) void upsertLastNotifiedArticleId( @Param("userId") Integer userId,