From a6cf88eca06c5971266e33dd629789817779795b Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Fri, 31 Jul 2026 17:00:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=9D=B8=EA=B8=B0=20=EC=A7=80?= =?UTF-8?q?=EC=97=AD=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0,=20=EC=9A=B4=EC=98=81=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=20=EA=B0=84=EC=86=8C=ED=99=94=20-=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=EC=A7=80=EC=97=AD=20=EC=A1=B0=ED=9A=8C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=88=98=EC=A4=80=20read-only=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A4=91=EB=8B=A8=20-?= =?UTF-8?q?=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=A1=B0=ED=9A=8C/=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=EC=9D=84=20=EB=8F=85=EB=A6=BD=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20-=20=EB=B9=84?= =?UTF-8?q?=EA=B4=80=EC=A0=81=20=EC=9E=A0=EA=B8=88=EA=B3=BC=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=EC=83=B7=20=EC=A0=80=EC=9E=A5=EC=9D=98=20=EC=9B=90?= =?UTF-8?q?=EC=9E=90=EC=84=B1=20=EB=B3=B4=EC=9E=A5=20-=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=A0=84=ED=8C=8C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20Hibernate=20JDBC=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=EB=A1=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20-=20JSON=20=EC=98=88=EC=99=B8=20=EC=8A=A4=ED=83=9D=EC=9D=84?= =?UTF-8?q?=20=EC=9B=90=EC=9D=B8=20=EC=9A=B0=EC=84=A0=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20-=20=EC=98=88=EC=99=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EA=B8=B8=EC=9D=B4=EC=99=80=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=9E=84=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/resources/logback-spring.xml | 22 +++++++++++++-- .../location/application/LocationService.java | 1 + .../application/LocationServiceTest.java | 15 +++++++++++ .../PopularLocationSnapshotService.java | 5 ++-- .../PopularLocationSnapshotServiceTest.java | 27 +++++++++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/src/main/resources/logback-spring.xml b/app/src/main/resources/logback-spring.xml index c3b08e4..1c57c8c 100644 --- a/app/src/main/resources/logback-spring.xml +++ b/app/src/main/resources/logback-spring.xml @@ -46,8 +46,18 @@ + + - + + + 20 + 4096 + 40 + true + true + + @@ -58,7 +68,15 @@ 14 1GB - + + + 20 + 4096 + 40 + true + true + + diff --git a/location/src/main/java/com/nalssilog/location/application/LocationService.java b/location/src/main/java/com/nalssilog/location/application/LocationService.java index f7a27cb..4361989 100644 --- a/location/src/main/java/com/nalssilog/location/application/LocationService.java +++ b/location/src/main/java/com/nalssilog/location/application/LocationService.java @@ -73,6 +73,7 @@ public LocationInfo reverseGeocode(double latitude, double longitude) { return locationRepository.findOrCreate(region); } + @Transactional(propagation = Propagation.NOT_SUPPORTED) public PopularLocationSnapshotInfo getPopular() { PopularLocationSnapshotData snapshot = popularLocationSource.latestSnapshot(); List locationIds = snapshot.rankings().stream() diff --git a/location/src/test/java/com/nalssilog/location/application/LocationServiceTest.java b/location/src/test/java/com/nalssilog/location/application/LocationServiceTest.java index 0f12a12..812e743 100644 --- a/location/src/test/java/com/nalssilog/location/application/LocationServiceTest.java +++ b/location/src/test/java/com/nalssilog/location/application/LocationServiceTest.java @@ -21,6 +21,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; @SuppressWarnings("java:S5960") // 표준 src/test 소스의 AssertJ 검증을 운영 코드 assertion으로 오인하는 경고. class LocationServiceTest { @@ -34,6 +36,19 @@ class LocationServiceTest { kakaoMapClient ); + @Test + void popularLookupSuspendsTheClassLevelReadOnlyTransaction() throws NoSuchMethodException { + AnnotationTransactionAttributeSource attributeSource = + new AnnotationTransactionAttributeSource(); + var transactionAttribute = attributeSource.getTransactionAttribute( + LocationService.class.getMethod("getPopular"), + LocationService.class); + + assertThat(transactionAttribute).isNotNull(); + assertThat(transactionAttribute.getPropagationBehavior()) + .isEqualTo(TransactionDefinition.PROPAGATION_NOT_SUPPORTED); + } + @Test void resolvesKakaoRegionAndReturnsPersistedLocation() { KakaoRegion region = new KakaoRegion( diff --git a/report/src/main/java/com/nalssilog/report/application/PopularLocationSnapshotService.java b/report/src/main/java/com/nalssilog/report/application/PopularLocationSnapshotService.java index 9e8c08a..c45447e 100644 --- a/report/src/main/java/com/nalssilog/report/application/PopularLocationSnapshotService.java +++ b/report/src/main/java/com/nalssilog/report/application/PopularLocationSnapshotService.java @@ -8,6 +8,7 @@ import java.util.stream.Collectors; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.nalssilog.location.application.dto.PopularLocationSnapshotData; @@ -54,7 +55,7 @@ private static PopularRankMovement movement(Integer rankChange) { return PopularRankMovement.SAME; } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public PopularLocationSnapshotData latestOrRefreshAt(Instant now) { PopularLocationSnapshot latest = snapshotRepository .findFirstByOrderByCalculatedAtDescIdDesc() @@ -77,7 +78,7 @@ public PopularLocationSnapshotData latestOrRefreshAt(Instant now) { return createSnapshot(now, latest); } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) public PopularLocationSnapshotData captureAt(Instant calculatedAt) { lockRepository.acquire(); diff --git a/report/src/test/java/com/nalssilog/report/application/PopularLocationSnapshotServiceTest.java b/report/src/test/java/com/nalssilog/report/application/PopularLocationSnapshotServiceTest.java index f86d0d2..31c6a58 100644 --- a/report/src/test/java/com/nalssilog/report/application/PopularLocationSnapshotServiceTest.java +++ b/report/src/test/java/com/nalssilog/report/application/PopularLocationSnapshotServiceTest.java @@ -23,6 +23,8 @@ import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; @SuppressWarnings("java:S5960") class PopularLocationSnapshotServiceTest { @@ -49,6 +51,31 @@ class PopularLocationSnapshotServiceTest { lockRepository, PROPERTIES); + @Test + void snapshotWritesAlwaysUseIndependentTransactions() throws NoSuchMethodException { + AnnotationTransactionAttributeSource attributeSource = + new AnnotationTransactionAttributeSource(); + var refreshAttribute = attributeSource.getTransactionAttribute( + PopularLocationSnapshotService.class.getMethod( + "latestOrRefreshAt", + Instant.class), + PopularLocationSnapshotService.class); + var captureAttribute = attributeSource.getTransactionAttribute( + PopularLocationSnapshotService.class.getMethod( + "captureAt", + Instant.class), + PopularLocationSnapshotService.class); + + assertThat(refreshAttribute).isNotNull(); + assertThat(refreshAttribute.getPropagationBehavior()) + .isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + assertThat(refreshAttribute.isReadOnly()).isFalse(); + assertThat(captureAttribute).isNotNull(); + assertThat(captureAttribute.getPropagationBehavior()) + .isEqualTo(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + assertThat(captureAttribute.isReadOnly()).isFalse(); + } + @Test void createsFirstSnapshotWithNewMovementsAndDetailedMetrics() { Instant calculatedAt = Instant.parse("2026-07-30T06:00:00Z"); From ea26c4fce1c7a65dc46a6b77f324776e17f306d8 Mon Sep 17 00:00:00 2001 From: fakerdeft Date: Fri, 31 Jul 2026 17:04:19 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=200.2.1=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9e6eb6d..7b6cf32 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -appVersion=0.2.0 +appVersion=0.2.1