From 4dfb5a2fce2ea11ed2a5220183f87ad221cb14f4 Mon Sep 17 00:00:00 2001 From: kswdot Date: Sat, 6 Dec 2025 11:25:02 +0900 Subject: [PATCH 1/5] test/#redis --- build.gradle | 1 + .../global/config/RedisConfig.java | 30 +++++++++++++++++++ src/main/resources/application.yml | 5 ++++ .../core_banking/domain/redis/RedisTest.java | 20 +++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/org/creditto/core_banking/global/config/RedisConfig.java create mode 100644 src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java diff --git a/build.gradle b/build.gradle index b8ffdd6..c095ddf 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.security:spring-security-crypto' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.micrometer:micrometer-tracing-bridge-brave' implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'io.zipkin.reporter2:zipkin-reporter-brave' diff --git a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java new file mode 100644 index 0000000..81f6669 --- /dev/null +++ b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java @@ -0,0 +1,30 @@ +package org.creditto.core_banking.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // key:value + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + // hash key:value + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer((new GenericJackson2JsonRedisSerializer())); + + template.afterPropertiesSet(); + + return template; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1cd215b..baa0958 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,6 +6,11 @@ spring: username: ${DB_USERNAME} password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: localhost + port: 6379 + timeout: scheduler: remittance: diff --git a/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java b/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java new file mode 100644 index 0000000..f637ee1 --- /dev/null +++ b/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java @@ -0,0 +1,20 @@ +package org.creditto.core_banking.domain.redis; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; + +@SpringBootTest +public class RedisTest { + + @Autowired + private StringRedisTemplate redisTemplate; + + @Test + void testRedisConnection() { + redisTemplate.opsForValue().set("test-key", "redis"); + String value = redisTemplate.opsForValue().get("test-key"); + System.out.println("Redis Value: " + value); + } +} From 49c7cffa97f503eb431ee1e85ad8dc5a035e5325 Mon Sep 17 00:00:00 2001 From: kswdot Date: Sat, 6 Dec 2025 11:34:36 +0900 Subject: [PATCH 2/5] test/#redis --- .../core_banking/global/config/RedisConfig.java | 2 -- src/main/resources/application.yml | 2 +- .../core_banking/domain/redis/RedisTest.java | 12 ++++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java index 81f6669..7e7b0e5 100644 --- a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java +++ b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java @@ -23,8 +23,6 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer((new GenericJackson2JsonRedisSerializer())); - template.afterPropertiesSet(); - return template; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index baa0958..1bfe977 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: redis: host: localhost port: 6379 - timeout: + timeout: 2000 scheduler: remittance: diff --git a/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java b/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java index f637ee1..3f1e3fb 100644 --- a/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java +++ b/src/test/java/org/creditto/core_banking/domain/redis/RedisTest.java @@ -1,20 +1,24 @@ package org.creditto.core_banking.domain.redis; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.RedisTemplate; +@Disabled @SpringBootTest public class RedisTest { @Autowired - private StringRedisTemplate redisTemplate; + private RedisTemplate redisTemplate; @Test void testRedisConnection() { - redisTemplate.opsForValue().set("test-key", "redis"); - String value = redisTemplate.opsForValue().get("test-key"); + String key = "test-key"; + String expectedValue = "redis"; + redisTemplate.opsForValue().set(key, expectedValue); + Object value = redisTemplate.opsForValue().get(key); System.out.println("Redis Value: " + value); } } From 5cb6afd08a0179855afdc034f3b1c01b9c4d05fc Mon Sep 17 00:00:00 2001 From: geonae Date: Sat, 6 Dec 2025 20:25:34 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=86=A1=EA=B8=88=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EB=B0=8F=20=EC=A0=95=EA=B8=B0=EC=86=A1=EA=B8=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20redis=20?= =?UTF-8?q?=EC=BA=90=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RemittanceProcessorService.java | 11 +++++++++ .../service/RemittanceQueryService.java | 18 +++++++++++++- .../service/RegularRemittanceService.java | 24 +++++++++++++++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java index c71072f..892a2d9 100644 --- a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java +++ b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceProcessorService.java @@ -25,6 +25,7 @@ import org.creditto.core_banking.global.common.CurrencyCode; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,6 +51,7 @@ public class RemittanceProcessorService { private final ExchangeService exchangeService; private final TransactionService transactionService; private final RemittanceFeeService remittanceFeeService; + private final RedisTemplate redisTemplate; /** * 전달된 Command를 기반으로 해외송금의 모든 단계를 실행합니다. @@ -135,6 +137,15 @@ public OverseasRemittanceResponseDto execute(final ExecuteRemittanceCommand comm accountRepository.save(account); + // 정기 송금일 경우 + if (command.regRemId() != null) { + String regularRemittanceHistoryKey = "regularRemittanceHistory::" + command.regRemId(); + redisTemplate.delete(regularRemittanceHistoryKey); + } else { // 일회성 송금일 경우 + String oneTimeRemittanceHistoryKey = "oneTimeRemittanceHistories::" + userId; + redisTemplate.delete(oneTimeRemittanceHistoryKey); + } + return OverseasRemittanceResponseDto.from(overseasRemittance); } diff --git a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceQueryService.java b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceQueryService.java index 828065a..786626e 100644 --- a/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceQueryService.java +++ b/src/main/java/org/creditto/core_banking/domain/overseasremittance/service/RemittanceQueryService.java @@ -5,6 +5,7 @@ import org.creditto.core_banking.domain.overseasremittance.dto.OverseasRemittanceResponseDto; import org.creditto.core_banking.domain.overseasremittance.repository.OverseasRemittanceRepository; import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +21,7 @@ public class RemittanceQueryService { private final OverseasRemittanceRepository remittanceRepository; + private final RedisTemplate redisTemplate; /** * 특정 고객의 모든 해외송금 내역을 조회합니다. @@ -28,10 +30,24 @@ public class RemittanceQueryService { * @return 고객의 송금 내역 DTO 리스트 */ public List getRemittanceList(Long userId) { - return remittanceRepository.findByUserIdWithDetails(userId) + String key = "oneTimeRemittanceHistories::" + userId; + + // redis에서 캐시 확인 + List cachedList = (List) redisTemplate.opsForValue().get(key); + if (cachedList != null) { + return cachedList; + } + + // 캐시 없으면 DB 조회 + List dbList = remittanceRepository.findByUserIdWithDetails(userId) .stream() .map(OverseasRemittanceResponseDto::from) .toList(); + + // 결과를 redis에 저장 + redisTemplate.opsForValue().set(key, dbList); + + return dbList; } /** diff --git a/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java b/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java index 903698c..8a27b83 100644 --- a/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java +++ b/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java @@ -15,6 +15,7 @@ import org.creditto.core_banking.domain.regularremittance.repository.RegularRemittanceRepository; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +32,7 @@ public class RegularRemittanceService { private final OverseasRemittanceRepository overseasRemittanceRepository; private final AccountRepository accountRepository; private final RecipientFactory recipientFactory; + private final RedisTemplate redisTemplate; /** * 특정 사용자의 모든 정기송금 설정 내역을 조회합니다. @@ -97,12 +99,21 @@ public RemittanceDetailDto getScheduledRemittanceDetail(Long userId, Long regRem * @return 해당 정기송금 설정에 대한 모든 송금 기록 목록 ({@link RemittanceHistoryDto}) */ public List getRegularRemittanceHistoryByRegRemId(Long userId, Long regRemId) { + String key = "regularRemittanceHistory::" + regRemId; + + // redis에서 캐시 확인 + List cachedList = (List) redisTemplate.opsForValue().get(key); + if (cachedList != null) { + return cachedList; + } + + // 캐시 없으면 DB 조회 RegularRemittance regularRemittance = regularRemittanceRepository.findById(regRemId) .orElseThrow(() -> new CustomBaseException(ErrorBaseCode.NOT_FOUND_REGULAR_REMITTANCE)); verifyUserOwnership(regularRemittance.getAccount().getUserId(), userId); - return overseasRemittanceRepository.findByRecur_RegRemIdOrderByCreatedAtDesc(regRemId).stream() + List dbList = overseasRemittanceRepository.findByRecur_RegRemIdOrderByCreatedAtDesc(regRemId).stream() .map(overseas -> new RemittanceHistoryDto( overseas.getRemittanceId(), overseas.getSendAmount(), @@ -110,6 +121,11 @@ public List getRegularRemittanceHistoryByRegRemId(Long use overseas.getCreatedAt().toLocalDate() )) .toList(); + + // 결과를 redis에 저장 + redisTemplate.opsForValue().set(key, dbList); + + return dbList; } /** @@ -220,10 +236,12 @@ public void updateScheduledRemittance(Long regRemId, Long userId, RegularRemitta } else if (remittance instanceof WeeklyRegularRemittance weekly) { weekly.updateSchedule(dto.getScheduledDay()); } + // 정기송금 "설정"이 변경되었으므로, 관련 "내역" 캐시도 삭제 + redisTemplate.delete("regularRemittanceHistory::" + regRemId); } /** - * 기존 정기 해외송금 설정을 삭제합니다. + * 기존 정기 해외송금 설정을 삭제합니다。 * * @param regRemId 삭제할 정기송금의 ID * @param userId 사용자 ID @@ -235,6 +253,8 @@ public void deleteScheduledRemittance(Long regRemId, Long userId) { verifyUserOwnership(remittance.getAccount().getUserId(), userId); regularRemittanceRepository.delete(remittance); + // 정기송금 "설정"이 삭제되었으므로, 관련 "내역" 캐시도 삭제 + redisTemplate.delete("regularRemittanceHistory::" + regRemId); } private void verifyUserOwnership(Long ownerId, Long requesterId) { From a2018e11dd19df0c4680289b2693d3ff0ee38711 Mon Sep 17 00:00:00 2001 From: geonae Date: Sat, 6 Dec 2025 22:29:29 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test:=20RegularRemittanceServiceTest=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RedisConfig.java | 2 +- .../service/RegularRemittanceServiceTest.java | 106 +++++++++++++++--- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java index 7e7b0e5..933c59d 100644 --- a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java +++ b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java @@ -25,4 +25,4 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec return template; } -} +} \ No newline at end of file diff --git a/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java b/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java index 807ce16..656e9fd 100644 --- a/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java +++ b/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java @@ -21,23 +21,28 @@ import org.creditto.core_banking.global.common.CurrencyCode; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.DayOfWeek; import java.time.LocalDate; import java.util.List; +import java.util.Objects; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.springframework.test.annotation.DirtiesContext; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; @SpringBootTest @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -52,12 +57,16 @@ class RegularRemittanceServiceTest { @Autowired private RecipientRepository recipientRepository; @Autowired - private OverseasRemittanceRepository overseasRemittanceRepository; - @Autowired private ExchangeRepository exchangeRepository; @Autowired private FeeRecordRepository feeRecordRepository; + @Autowired + private OverseasRemittanceRepository overseasRemittanceRepository; + + @Autowired + private RedisTemplate redisTemplate; + private Long testUserId = 3L; private Long otherUserId = 4L; @@ -98,6 +107,12 @@ void setup() { regularRemittanceRepository.save(MonthlyRegularRemittance.of(otherAccount, otherRecipient, CurrencyCode.KRW, CurrencyCode.USD, BigDecimal.valueOf(500), 10, startedAt)); } + @AfterEach + void tearDown() { + // 각 테스트 후 Redis 데이터 삭제하여 격리성 보장 + Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection().flushDb(); + } + @Test @Transactional @DisplayName("사용자 ID로 정기송금 설정 내역 조회") @@ -125,7 +140,7 @@ void getRegularRemittanceHistoryByRegRemId_Success() { void getRegularRemittanceHistoryByRegRemId_Forbidden() { assertThrows(CustomBaseException.class, () -> regularRemittanceService.getRegularRemittanceHistoryByRegRemId(otherUserId, testMonthlyRemittance.getRegRemId())); } - + @Test @Transactional @DisplayName("정기송금 상세 조회 (월간) - 성공") @@ -168,7 +183,7 @@ void getScheduledRemittanceDetail_Weekly_Success() { void getScheduledRemittanceDetail_Forbidden() { // when & then CustomBaseException exception = assertThrows(CustomBaseException.class, - () -> regularRemittanceService.getScheduledRemittanceDetail(otherUserId, testMonthlyRemittance.getRegRemId())); + () -> regularRemittanceService.getScheduledRemittanceDetail(otherUserId, testMonthlyRemittance.getRegRemId())); assertThat(exception.getErrorCode()).isEqualTo(ErrorBaseCode.FORBIDDEN); } @@ -178,7 +193,7 @@ void getScheduledRemittanceDetail_Forbidden() { void getScheduledRemittanceDetail_NotFound() { // when & then CustomBaseException exception = assertThrows(CustomBaseException.class, - () -> regularRemittanceService.getScheduledRemittanceDetail(testUserId, 999L)); + () -> regularRemittanceService.getScheduledRemittanceDetail(testUserId, 999L)); assertThat(exception.getErrorCode()).isEqualTo(ErrorBaseCode.NOT_FOUND_REGULAR_REMITTANCE); } @@ -271,18 +286,29 @@ void createScheduledRemittance_WeeklyDuplicate() { @Test @Transactional - @DisplayName("정기송금 설정 수정") + @DisplayName("정기송금 설정 수정 시 캐시 삭제") void updateScheduledRemittance_Success() { + // given + Long regRemId = testMonthlyRemittance.getRegRemId(); + String key = "regularRemittanceHistory::" + regRemId; + redisTemplate.opsForValue().set(key, List.of()); // 미리 캐시를 저장해둠 + RegularRemittanceUpdateDto updateDto = new RegularRemittanceUpdateDto( testAccount.getAccountNo(), BigDecimal.valueOf(1500), RegRemStatus.PAUSED, 25, null ); - regularRemittanceService.updateScheduledRemittance(testMonthlyRemittance.getRegRemId(), testUserId, updateDto); + // when + regularRemittanceService.updateScheduledRemittance(regRemId, testUserId, updateDto); - RegularRemittance updated = regularRemittanceRepository.findById(testMonthlyRemittance.getRegRemId()).get(); + // then + RegularRemittance updated = regularRemittanceRepository.findById(regRemId).get(); assertThat(updated.getSendAmount()).isEqualByComparingTo("1500"); assertThat(updated.getRegRemStatus()).isEqualTo(RegRemStatus.PAUSED); assertThat(((MonthlyRegularRemittance) updated).getScheduledDate()).isEqualTo(25); + + // 캐시 삭제 검증 + Object cachedValue = redisTemplate.opsForValue().get(key); + assertThat(cachedValue).isNull(); } @Test @@ -298,13 +324,23 @@ void updateScheduledRemittance_Forbidden() { @Test @Transactional - @DisplayName("정기송금 설정 삭제") + @DisplayName("정기송금 설정 삭제 시 캐시 삭제") void deleteScheduledRemittance_Success() { + // given Long regRemId = testMonthlyRemittance.getRegRemId(); + String key = "regularRemittanceHistory::" + regRemId; + redisTemplate.opsForValue().set(key, List.of()); // 미리 캐시를 저장해둠 + + // when regularRemittanceService.deleteScheduledRemittance(regRemId, testUserId); + // then Optional deleted = regularRemittanceRepository.findById(regRemId); assertThat(deleted).isNotPresent(); + + // 캐시 삭제 검증 + Object cachedValue = redisTemplate.opsForValue().get(key); + assertThat(cachedValue).isNull(); } @Test @@ -326,10 +362,54 @@ void createScheduledRemittance_PersistsStartedAt() { RegularRemittanceResponseDto result = regularRemittanceService.createScheduledRemittance(testUserId, createDto); - // Retrieve the entity directly from the repository RegularRemittance savedRemittance = regularRemittanceRepository.findById(result.getRegRemId()) .orElseThrow(() -> new AssertionError("Saved remittance not found")); assertThat(savedRemittance.getStartedAt()).isEqualTo(expectedStartedAt); } -} \ No newline at end of file + + // --- 캐싱 관련 새로운 테스트 --- + + @Test + @Transactional + @DisplayName("정기송금 내역 조회 - Cache Miss (캐시 없음)") + void getRegularRemittanceHistoryByRegRemId_CacheMiss() { + // given + Long regRemId = testMonthlyRemittance.getRegRemId(); + String key = "regularRemittanceHistory::" + regRemId; + + // when + regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, regRemId); + + // then + // 1. DB를 조회했는지 검증 (SpyBean) + verify(overseasRemittanceRepository, times(1)).findByRecur_RegRemIdOrderByCreatedAtDesc(regRemId); + // 2. Redis에 값이 저장되었는지 검증 + Object cachedValue = redisTemplate.opsForValue().get(key); + assertThat(cachedValue).isNotNull(); + assertThat((List) cachedValue).hasSize(1); + } + + @Test + @Transactional + @DisplayName("정기송금 내역 조회 - Cache Hit (캐시 있음)") + void getRegularRemittanceHistoryByRegRemId_CacheHit() { + // given + Long regRemId = testMonthlyRemittance.getRegRemId(); + String key = "regularRemittanceHistory::" + regRemId; + + // 미리 Redis에 가짜 데이터 저장 + List fakeCachedList = List.of(new RemittanceHistoryDto(999L, BigDecimal.TEN, BigDecimal.ONE, LocalDate.now())); + redisTemplate.opsForValue().set(key, fakeCachedList); + + // when + List result = regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, regRemId); + + // then + // 1. 반환된 결과가 캐시된 데이터와 동일한지 검증 (DB 데이터가 아님을 증명) + assertThat(result.get(0).getRemittanceId()).isEqualTo(999L); + + // 2. DB를 조회하지 않았는지 검증 (SpyBean) + verify(overseasRemittanceRepository, never()).findByRecur_RegRemIdOrderByCreatedAtDesc(anyLong()); + } +} From e32172a4306c0b42b7242c206c1674179be1a4d0 Mon Sep 17 00:00:00 2001 From: geonae Date: Sun, 7 Dec 2025 01:50:02 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20RegularRemittanceServiceTest=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RemittanceHistoryDto.java | 13 +- .../service/RegularRemittanceService.java | 36 ++++- .../global/config/RedisConfig.java | 13 +- .../service/RegularRemittanceServiceTest.java | 146 +++++++----------- 4 files changed, 107 insertions(+), 101 deletions(-) diff --git a/src/main/java/org/creditto/core_banking/domain/regularremittance/dto/RemittanceHistoryDto.java b/src/main/java/org/creditto/core_banking/domain/regularremittance/dto/RemittanceHistoryDto.java index e5f8e74..a3afcfa 100644 --- a/src/main/java/org/creditto/core_banking/domain/regularremittance/dto/RemittanceHistoryDto.java +++ b/src/main/java/org/creditto/core_banking/domain/regularremittance/dto/RemittanceHistoryDto.java @@ -1,18 +1,19 @@ package org.creditto.core_banking.domain.regularremittance.dto; +import lombok.AccessLevel; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.time.LocalDate; @Getter -@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class RemittanceHistoryDto { - Long remittanceId; - BigDecimal sendAmount; - BigDecimal exchangeRate; - LocalDate createdDate; + private Long remittanceId; + private BigDecimal sendAmount; + private BigDecimal exchangeRate; + private LocalDate createdDate; } diff --git a/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java b/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java index 8a27b83..cbe094d 100644 --- a/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java +++ b/src/main/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceService.java @@ -1,6 +1,11 @@ package org.creditto.core_banking.domain.regularremittance.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.account.repository.AccountRepository; import org.creditto.core_banking.domain.overseasremittance.entity.OverseasRemittance; @@ -15,6 +20,8 @@ import org.creditto.core_banking.domain.regularremittance.repository.RegularRemittanceRepository; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,16 +30,20 @@ import java.util.List; import java.util.Objects; +@Slf4j @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class RegularRemittanceService { + private static final Logger log = LoggerFactory.getLogger(RegularRemittanceService.class); private final RegularRemittanceRepository regularRemittanceRepository; private final OverseasRemittanceRepository overseasRemittanceRepository; private final AccountRepository accountRepository; private final RecipientFactory recipientFactory; private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + /** * 특정 사용자의 모든 정기송금 설정 내역을 조회합니다. @@ -101,10 +112,16 @@ public RemittanceDetailDto getScheduledRemittanceDetail(Long userId, Long regRem public List getRegularRemittanceHistoryByRegRemId(Long userId, Long regRemId) { String key = "regularRemittanceHistory::" + regRemId; - // redis에서 캐시 확인 - List cachedList = (List) redisTemplate.opsForValue().get(key); - if (cachedList != null) { - return cachedList; + try { + Object cachedObject = redisTemplate.opsForValue().get(key); + if (cachedObject != null) { + if (cachedObject instanceof String) { + return objectMapper.readValue((String) cachedObject, new TypeReference>() {}); + } + return (List) cachedObject; + } + } catch (Exception e) { + log.error("Redis cache deserialization error", e); } // 캐시 없으면 DB 조회 @@ -122,8 +139,13 @@ public List getRegularRemittanceHistoryByRegRemId(Long use )) .toList(); - // 결과를 redis에 저장 - redisTemplate.opsForValue().set(key, dbList); + try { + // 결과를 JSON 문자열로 변환하여 Redis에 저장 + String jsonString = objectMapper.writeValueAsString(dbList); + redisTemplate.opsForValue().set(key, jsonString); + } catch (JsonProcessingException e) { + log.error("Redis cache serialization error", e); + } return dbList; } @@ -293,4 +315,4 @@ private boolean hasDuplicateRemittance(Account account, Recipient recipient, Reg return false; } -} +} \ No newline at end of file diff --git a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java index 933c59d..69a4d7f 100644 --- a/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java +++ b/src/main/java/org/creditto/core_banking/global/config/RedisConfig.java @@ -1,5 +1,7 @@ package org.creditto.core_banking.global.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -15,13 +17,20 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); + // LocalDate, LocalDateTime 직렬화를 위한 ObjectMapper 설정 + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + // JavaTimeModule이 등록된 ObjectMapper를 사용하는 Serializer 생성 + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); + // key:value template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setValueSerializer(serializer); // hash key:value template.setHashKeySerializer(new StringRedisSerializer()); - template.setHashValueSerializer((new GenericJackson2JsonRedisSerializer())); + template.setHashValueSerializer(serializer); return template; } diff --git a/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java b/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java index 656e9fd..d82ca16 100644 --- a/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java +++ b/src/test/java/org/creditto/core_banking/domain/regularremittance/service/RegularRemittanceServiceTest.java @@ -1,5 +1,8 @@ package org.creditto.core_banking.domain.regularremittance.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.creditto.core_banking.domain.account.entity.Account; import org.creditto.core_banking.domain.account.entity.AccountState; import org.creditto.core_banking.domain.account.entity.AccountType; @@ -21,28 +24,24 @@ import org.creditto.core_banking.global.common.CurrencyCode; import org.creditto.core_banking.global.response.error.ErrorBaseCode; import org.creditto.core_banking.global.response.exception.CustomBaseException; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.DayOfWeek; import java.time.LocalDate; import java.util.List; -import java.util.Objects; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; + +import org.springframework.test.annotation.DirtiesContext; @SpringBootTest @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @@ -57,16 +56,17 @@ class RegularRemittanceServiceTest { @Autowired private RecipientRepository recipientRepository; @Autowired + private OverseasRemittanceRepository overseasRemittanceRepository; + @Autowired private ExchangeRepository exchangeRepository; @Autowired private FeeRecordRepository feeRecordRepository; - @Autowired - private OverseasRemittanceRepository overseasRemittanceRepository; - @Autowired private RedisTemplate redisTemplate; + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + private Long testUserId = 3L; private Long otherUserId = 4L; @@ -107,12 +107,6 @@ void setup() { regularRemittanceRepository.save(MonthlyRegularRemittance.of(otherAccount, otherRecipient, CurrencyCode.KRW, CurrencyCode.USD, BigDecimal.valueOf(500), 10, startedAt)); } - @AfterEach - void tearDown() { - // 각 테스트 후 Redis 데이터 삭제하여 격리성 보장 - Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection().flushDb(); - } - @Test @Transactional @DisplayName("사용자 ID로 정기송금 설정 내역 조회") @@ -215,6 +209,51 @@ void getRegularRemittanceDetail_Forbidden() { assertThrows(CustomBaseException.class, () -> regularRemittanceService.getRemittanceHistoryDetail(otherUserId, testOverseasRemittance.getRemittanceId(), testMonthlyRemittance.getRegRemId())); } + @Test + @Transactional + @DisplayName("정기송금 내역 조회 - Cache Miss") + void getRegularRemittanceHistoryByRegRemId_CacheMiss() throws JsonProcessingException { + // given + Long regRemId = testMonthlyRemittance.getRegRemId(); + String key = "regularRemittanceHistory::" + regRemId; + + // when + List result = regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, regRemId); + + // then + // 1. 반환된 결과가 DB의 실제 데이터와 일치하는지 검증 + assertThat(result).hasSize(1); + assertThat(result.get(0).getRemittanceId()).isEqualTo(testOverseasRemittance.getRemittanceId()); + + // 2. Redis에 값이 JSON 문자열 형태로 저장되었는지 검증 + String cachedJson = (String) redisTemplate.opsForValue().get(key); + assertThat(cachedJson).isNotNull(); + assertThat(objectMapper.writeValueAsString(result)).isEqualTo(cachedJson); + } + + @Test + @DisplayName("정기송금 내역 조회 - Cache Hit") + void getRegularRemittanceHistoryByRegRemId_CacheHit() throws JsonProcessingException { + // given + // DB에 없는 ID로 캐시 데이터를 준비 + Long nonExistentRegRemId = 999L; + String key = "regularRemittanceHistory::" + nonExistentRegRemId; + + // 미리 Redis에 식별 가능한 가짜 데이터를 JSON 문자열로 저장 + List fakeCachedList = List.of(new RemittanceHistoryDto(999L, BigDecimal.TEN, BigDecimal.ONE, LocalDate.now())); + String fakeJson = objectMapper.writeValueAsString(fakeCachedList); + redisTemplate.opsForValue().set(key, fakeJson); + + // when + List result = regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, nonExistentRegRemId); + + // then + // 1. 반환된 결과가 캐시된 데이터와 동일한지 검증 + assertThat(result).hasSize(1); + assertThat(result.get(0).getRemittanceId()).isEqualTo(999L); + assertThat(result.get(0).getSendAmount()).isEqualByComparingTo(BigDecimal.TEN); + } + @Test @Transactional @DisplayName("월간 정기송금 신규 등록") @@ -286,29 +325,18 @@ void createScheduledRemittance_WeeklyDuplicate() { @Test @Transactional - @DisplayName("정기송금 설정 수정 시 캐시 삭제") + @DisplayName("정기송금 설정 수정") void updateScheduledRemittance_Success() { - // given - Long regRemId = testMonthlyRemittance.getRegRemId(); - String key = "regularRemittanceHistory::" + regRemId; - redisTemplate.opsForValue().set(key, List.of()); // 미리 캐시를 저장해둠 - RegularRemittanceUpdateDto updateDto = new RegularRemittanceUpdateDto( testAccount.getAccountNo(), BigDecimal.valueOf(1500), RegRemStatus.PAUSED, 25, null ); - // when - regularRemittanceService.updateScheduledRemittance(regRemId, testUserId, updateDto); + regularRemittanceService.updateScheduledRemittance(testMonthlyRemittance.getRegRemId(), testUserId, updateDto); - // then - RegularRemittance updated = regularRemittanceRepository.findById(regRemId).get(); + RegularRemittance updated = regularRemittanceRepository.findById(testMonthlyRemittance.getRegRemId()).get(); assertThat(updated.getSendAmount()).isEqualByComparingTo("1500"); assertThat(updated.getRegRemStatus()).isEqualTo(RegRemStatus.PAUSED); assertThat(((MonthlyRegularRemittance) updated).getScheduledDate()).isEqualTo(25); - - // 캐시 삭제 검증 - Object cachedValue = redisTemplate.opsForValue().get(key); - assertThat(cachedValue).isNull(); } @Test @@ -324,23 +352,13 @@ void updateScheduledRemittance_Forbidden() { @Test @Transactional - @DisplayName("정기송금 설정 삭제 시 캐시 삭제") + @DisplayName("정기송금 설정 삭제") void deleteScheduledRemittance_Success() { - // given Long regRemId = testMonthlyRemittance.getRegRemId(); - String key = "regularRemittanceHistory::" + regRemId; - redisTemplate.opsForValue().set(key, List.of()); // 미리 캐시를 저장해둠 - - // when regularRemittanceService.deleteScheduledRemittance(regRemId, testUserId); - // then Optional deleted = regularRemittanceRepository.findById(regRemId); assertThat(deleted).isNotPresent(); - - // 캐시 삭제 검증 - Object cachedValue = redisTemplate.opsForValue().get(key); - assertThat(cachedValue).isNull(); } @Test @@ -362,54 +380,10 @@ void createScheduledRemittance_PersistsStartedAt() { RegularRemittanceResponseDto result = regularRemittanceService.createScheduledRemittance(testUserId, createDto); + // Retrieve the entity directly from the repository RegularRemittance savedRemittance = regularRemittanceRepository.findById(result.getRegRemId()) .orElseThrow(() -> new AssertionError("Saved remittance not found")); assertThat(savedRemittance.getStartedAt()).isEqualTo(expectedStartedAt); } - - // --- 캐싱 관련 새로운 테스트 --- - - @Test - @Transactional - @DisplayName("정기송금 내역 조회 - Cache Miss (캐시 없음)") - void getRegularRemittanceHistoryByRegRemId_CacheMiss() { - // given - Long regRemId = testMonthlyRemittance.getRegRemId(); - String key = "regularRemittanceHistory::" + regRemId; - - // when - regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, regRemId); - - // then - // 1. DB를 조회했는지 검증 (SpyBean) - verify(overseasRemittanceRepository, times(1)).findByRecur_RegRemIdOrderByCreatedAtDesc(regRemId); - // 2. Redis에 값이 저장되었는지 검증 - Object cachedValue = redisTemplate.opsForValue().get(key); - assertThat(cachedValue).isNotNull(); - assertThat((List) cachedValue).hasSize(1); - } - - @Test - @Transactional - @DisplayName("정기송금 내역 조회 - Cache Hit (캐시 있음)") - void getRegularRemittanceHistoryByRegRemId_CacheHit() { - // given - Long regRemId = testMonthlyRemittance.getRegRemId(); - String key = "regularRemittanceHistory::" + regRemId; - - // 미리 Redis에 가짜 데이터 저장 - List fakeCachedList = List.of(new RemittanceHistoryDto(999L, BigDecimal.TEN, BigDecimal.ONE, LocalDate.now())); - redisTemplate.opsForValue().set(key, fakeCachedList); - - // when - List result = regularRemittanceService.getRegularRemittanceHistoryByRegRemId(testUserId, regRemId); - - // then - // 1. 반환된 결과가 캐시된 데이터와 동일한지 검증 (DB 데이터가 아님을 증명) - assertThat(result.get(0).getRemittanceId()).isEqualTo(999L); - - // 2. DB를 조회하지 않았는지 검증 (SpyBean) - verify(overseasRemittanceRepository, never()).findByRecur_RegRemIdOrderByCreatedAtDesc(anyLong()); - } -} +} \ No newline at end of file