Skip to content

Commit

Permalink
feat: CacheSynchronizationService 추가 (#320)
Browse files Browse the repository at this point in the history
- LocalCache 데이터를 주기적으로 Redis로 동기화
- 비동기 처리를 통해 성능 병목 현상 방지
- 배치 사이즈를 작게 설정하여 처리 성능 최적화
  • Loading branch information
toychip committed Jan 2, 2025
1 parent f48387d commit b256ae9
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -81,28 +81,27 @@ 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;
}
}

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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class EmailController implements EmailControllerDocs {
public TtoklipResponse<Message> mailSend(
final @RequestBody EmailSendRequest request
) {
emailFacade.sendEmail(request.email());
emailFacade.send(request.email());
return TtoklipResponse.accepted(
Message.sendEmail()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> allKeys = localCacheRepository.getAllKeys();
List<String> keyList = new ArrayList<>(allKeys);
int batchSize = 10; // 한 번에 처리할 배치 크기

for (int i = 0; i < keyList.size(); i += batchSize) {
List<String> 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -29,4 +30,8 @@ public RedisConnectionFactory redisConnectionFactory() {
return redisTemplate;
}

@Bean
public StringRedisTemplate stringRedisTemplate() {
return new StringRedisTemplate(redisConnectionFactory());
}
}

This file was deleted.

0 comments on commit b256ae9

Please sign in to comment.