From b256ae9e9dc1b0ed80e407f1ebb7c93cc14e77ef Mon Sep 17 00:00:00 2001 From: toychip Date: Thu, 2 Jan 2025 20:32:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20CacheSynchronizationService=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#320)=20-=20LocalCache=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EC=A3=BC=EA=B8=B0=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20Redis=EB=A1=9C=20=EB=8F=99=EA=B8=B0=ED=99=94=20-=20?= =?UTF-8?q?=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EC=84=B1=EB=8A=A5=20=EB=B3=91=EB=AA=A9=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EB=B0=A9=EC=A7=80=20-=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=82=AC=EC=9D=B4=EC=A6=88=EB=A5=BC=20=EC=9E=91?= =?UTF-8?q?=EA=B2=8C=20=EC=84=A4=EC=A0=95=ED=95=98=EC=97=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/email/application/EmailFacade.java | 25 ++++--- .../email/presentation/EmailController.java | 2 +- .../service/CacheSynchronizationService.java | 66 +++++++++++++++++++ .../infrastructure/config/RedisConfig.java | 5 ++ .../com/infrastructure/util/RedisUtil.java | 33 ---------- 5 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 ttoklip-infrastructure/src/main/java/com/infrastructure/cache/service/CacheSynchronizationService.java delete mode 100644 ttoklip-infrastructure/src/main/java/com/infrastructure/util/RedisUtil.java diff --git a/ttoklip-api/src/main/java/com/api/email/application/EmailFacade.java b/ttoklip-api/src/main/java/com/api/email/application/EmailFacade.java index 03aa409f..5752683f 100644 --- a/ttoklip-api/src/main/java/com/api/email/application/EmailFacade.java +++ b/ttoklip-api/src/main/java/com/api/email/application/EmailFacade.java @@ -3,7 +3,7 @@ import com.api.email.presentation.EmailVerifyRequest; import com.common.exception.ApiException; import com.common.exception.ErrorType; -import com.infrastructure.util.RedisUtil; +import com.infrastructure.cache.repository.CacheRepository; import jakarta.mail.MessagingException; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; @@ -24,8 +24,8 @@ public class EmailFacade { private final Sender sender; - private final RedisUtil redisUtil; private final SpringTemplateEngine templateEngine; + private final CacheRepository cacheRepository; @Value("${spring.mail.sender-email}") private String senderEmail; @@ -72,7 +72,7 @@ private MimeMessage createEmailForm(String email) { private void setRedisData(final String email, final String authCode) { try { - redisUtil.setDataExpire(email, authCode, 60 * 30L); + cacheRepository.set(email, authCode, 60 * 30L); } catch (Exception e) { log.error("Failed to set Redis data: {}", e.getMessage(), e); throw new ApiException(ErrorType.REDIS_SAVE_ERROR); @@ -81,12 +81,12 @@ private void setRedisData(final String email, final String authCode) { @Async // 인증코드 이메일 발송 - public void sendEmail(String toEmail) { + public void send(String toEmail) { try { validEmailHasText(toEmail); validRedisHasEmail(toEmail); - - createEmail(toEmail); + MimeMessage email = createEmail(toEmail); + sendEmail(email); } catch (Exception e) { log.error("Exception in sendEmail: {}", e.getMessage(), e); throw e; @@ -94,15 +94,14 @@ public void sendEmail(String toEmail) { } private void validRedisHasEmail(final String toEmail) { - if (redisUtil.existData(toEmail)) { - redisUtil.deleteData(toEmail); + if (cacheRepository.exists(toEmail)) { + cacheRepository.delete(toEmail); } } - private void createEmail(final String toEmail) { + private MimeMessage createEmail(final String toEmail) { try { - MimeMessage emailForm = createEmailForm(toEmail); - sendEmail(emailForm); + return createEmailForm(toEmail); } catch (ApiException e) { log.error("ApiException during email creation: {}", e.getMessage(), e); throw e; @@ -139,8 +138,8 @@ private void validCode(String email, String code) { } try { - String codeFoundByEmail = redisUtil.getData(email); - log.info("code found by email: " + codeFoundByEmail); + String codeFoundByEmail = cacheRepository.get(email); + log.info("code found by email: {}", codeFoundByEmail); validAuthenticationCode(code, codeFoundByEmail); } catch (Exception e) { log.error("Failed to retrieve data from Redis: {}", e.getMessage(), e); diff --git a/ttoklip-api/src/main/java/com/api/email/presentation/EmailController.java b/ttoklip-api/src/main/java/com/api/email/presentation/EmailController.java index 18cd2b44..4ce96a06 100644 --- a/ttoklip-api/src/main/java/com/api/email/presentation/EmailController.java +++ b/ttoklip-api/src/main/java/com/api/email/presentation/EmailController.java @@ -21,7 +21,7 @@ public class EmailController implements EmailControllerDocs { public TtoklipResponse mailSend( final @RequestBody EmailSendRequest request ) { - emailFacade.sendEmail(request.email()); + emailFacade.send(request.email()); return TtoklipResponse.accepted( Message.sendEmail() ); diff --git a/ttoklip-infrastructure/src/main/java/com/infrastructure/cache/service/CacheSynchronizationService.java b/ttoklip-infrastructure/src/main/java/com/infrastructure/cache/service/CacheSynchronizationService.java new file mode 100644 index 00000000..f041a77f --- /dev/null +++ b/ttoklip-infrastructure/src/main/java/com/infrastructure/cache/service/CacheSynchronizationService.java @@ -0,0 +1,66 @@ +package com.infrastructure.cache.service; + +import com.infrastructure.cache.repository.LocalCacheRepository; +import com.infrastructure.cache.repository.RedisCacheRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * CacheSynchronizationService + * + * 이 서비스는 Redis Image가 일시적으로 다운되어 Local Cache가 사용된 경우, + * LocalCache 데이터를 주기적으로 Redis로 동기화하는 역할을 합니다. + * + * 주요 기능: + * 1. 5분마다 LocalCache 데이터를 Redis로 동기화 + * 2. 각 키를 비동기적으로 처리하여 성능 병목을 방지 + */ + +@Slf4j +@Service +@RequiredArgsConstructor +public class CacheSynchronizationService { + + private final RedisCacheRepository redisCacheRepository; + private final LocalCacheRepository localCacheRepository; + + /** + * 5분마다 LocalCache 데이터를 Redis로 동기화 + */ + @Scheduled(fixedRate = 300000) // 5분마다 실행 + public void syncLocalCacheToRedis() { + log.info("Starting batch synchronization of LocalCache to Redis..."); + Set allKeys = localCacheRepository.getAllKeys(); + List keyList = new ArrayList<>(allKeys); + int batchSize = 10; // 한 번에 처리할 배치 크기 + + for (int i = 0; i < keyList.size(); i += batchSize) { + List batch = keyList.subList(i, Math.min(i + batchSize, keyList.size())); + batch.forEach(this::syncKeyAsync); + } + } + + /** + * 개별 키를 Redis로 비동기 동기화 + */ + @Async + public void syncKeyAsync(String key) { + try { + String value = localCacheRepository.get(key); + if (value != null) { + redisCacheRepository.set(key, value, 60); // TTL 60초 + localCacheRepository.delete(key); // 동기화 후 LocalCache에서 제거 + log.info("Key '{}' synchronized to Redis.", key); + } + } catch (Exception e) { + log.warn("Failed to synchronize key '{}' to Redis: {}", key, e.getMessage()); + } + } +} diff --git a/ttoklip-infrastructure/src/main/java/com/infrastructure/config/RedisConfig.java b/ttoklip-infrastructure/src/main/java/com/infrastructure/config/RedisConfig.java index 11e959fc..2c0f0ec5 100644 --- a/ttoklip-infrastructure/src/main/java/com/infrastructure/config/RedisConfig.java +++ b/ttoklip-infrastructure/src/main/java/com/infrastructure/config/RedisConfig.java @@ -7,6 +7,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class RedisConfig { @@ -29,4 +30,8 @@ public RedisConnectionFactory redisConnectionFactory() { return redisTemplate; } + @Bean + public StringRedisTemplate stringRedisTemplate() { + return new StringRedisTemplate(redisConnectionFactory()); + } } diff --git a/ttoklip-infrastructure/src/main/java/com/infrastructure/util/RedisUtil.java b/ttoklip-infrastructure/src/main/java/com/infrastructure/util/RedisUtil.java deleted file mode 100644 index 12d62801..00000000 --- a/ttoklip-infrastructure/src/main/java/com/infrastructure/util/RedisUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.infrastructure.util; - -import java.time.Duration; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class RedisUtil { - - private final StringRedisTemplate template; - - public String getData(String key) { - ValueOperations valueOperations = template.opsForValue(); - return valueOperations.get(key); - } - - public boolean existData(String key) { - return Boolean.TRUE.equals(template.hasKey(key)); - } - - public void setDataExpire(String key, String value, long duration) { - ValueOperations valueOperations = template.opsForValue(); - Duration expireDuration = Duration.ofSeconds(duration); - valueOperations.set(key, value, expireDuration); - } - - public void deleteData(String key) { - template.delete(key); - } -}