Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions app/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,18 @@

<!-- prod: JSON 구조화 로그(콘솔 + 롤링 파일). MDC(traceId + 액세스 필드 method/path/status/durationMs/actor)를 그대로 필드화 -->
<springProfile name="prod">
<logger name="org.hibernate.orm.jdbc.error" level="ERROR"/>

<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>20</maxDepthPerThrowable>
<maxLength>4096</maxLength>
<shortenedClassNameLength>40</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
<omitCommonFrames>true</omitCommonFrames>
</throwableConverter>
</encoder>
</appender>

<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
Expand All @@ -58,7 +68,15 @@
<maxHistory>14</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
<maxDepthPerThrowable>20</maxDepthPerThrowable>
<maxLength>4096</maxLength>
<shortenedClassNameLength>40</shortenedClassNameLength>
<rootCauseFirst>true</rootCauseFirst>
<omitCommonFrames>true</omitCommonFrames>
</throwableConverter>
</encoder>
</appender>

<root level="INFO">
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
appVersion=0.2.0
appVersion=0.2.1
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long> locationIds = snapshot.rankings().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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");
Expand Down