카테고리 없음
Redis 활용
issac
2024. 6. 19. 15:40
사용 기술
- Redis(Bitnami)
- Docker Compose
- AOP
개선사항
- Refresh Token 을 기존 RDB 에서 NoSQL 로 구현하여 불필요한 부담을 줄인다.
- Cache를 WAS 서버가 아닌 레디스에서 관리한다.
Redis 설치
- Master 에 문제가 생기면 redis-slave-1 으로, redis-slave-1이 문제가 생기면 redis-slave-2 로 대응이 가능.
- 실제 운영서버에서는 레디스 서버별로 메모리 점유 제한을 추가로 주고 포트번호를 6379로 주지 않습니다.
(Active-Replication-Replication)으로 운영위한 docker-compose 세팅
version: '2'
networks:
dev-redis:
driver: bridge
services:
redis:
image: 'bitnami/redis:latest'
environment:
- REDIS_REPLICATION_MODE=master
- ALLOW_EMPTY_PASSWORD=yes
networks:
- dev-redis
ports:
- 6379:6379
redis-slave-1:
image: 'bitnami/redis:latest'
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis
- ALLOW_EMPTY_PASSWORD=yes
ports:
- 6479:6379
depends_on:
- redis
networks:
- dev-redis
redis-slave-2:
image: 'bitnami/redis:latest'
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis
- ALLOW_EMPTY_PASSWORD=yes
ports:
- 6579:6379
depends_on:
- redis
networks:
- dev-redis
docker-compose up 을 하여 활성화 시켜 간단하게 설치한다.
테스트
- Master 에서 Set 한 데이터가 Replication1, Replication2 에 동일하게 적재되어 있는지 확인한다.
- Replication1 확인
- Replication2 확인
캐싱관리
- 스프링에서 제공해주는 @Cacheable 을 사용하면 간단하게 캐싱 시켜둘수 있으나 TTL 과 함께 선언할 수 없고 메모리 관리까지 하기는 힘들다. 예) *@Cacheable(value = "file", key = "#id", cacheManager = "redisCacheManager")*
커스텀 어노테이션 선언
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
/* cache name. */
String name() default "";
/* key or dynamic key. ex) #id */
String key() default "";
/* data alive seconds. */
long timeToLive() default 60L;
/* data extension seconds. */
long extensionSeconds() default 60L;
}
커스텀 AOP 설정
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RedisCacheAspect {
private final RedisTemplate<String, Object> redisTemplate;
@Around("@annotation(com.x.utils.RedisCache)")
public Object customCacheable(ProceedingJoinPoint joinPoint) throws Throwable {
//1. custom annotation 을 찾습니다.
RedisCache redisCache = joinPoint.getTarget()
.getClass()
.getMethod(joinPoint.getSignature().getName(),
((MethodSignature) joinPoint.getSignature()).getParameterTypes())
.getAnnotation(RedisCache.class);
//2. #id 와 같이 동적인 아이디를 주는 경우 해당 아이디를 찾습니다.
String dynamicKey = getDynamicValue(
((MethodSignature) joinPoint.getSignature()).getParameterNames(), joinPoint.getArgs(),
redisCache.key());
//3. 키를 생성합니다.
String key = generateKey(redisCache.name(), dynamicKey);
long ttl = redisCache.timeToLive();
long es = redisCache.extensionSeconds();
//4. 키가 있는 경우 custom annotation 에 설정한 시간만큼 TTL을 늘려주고 캐싱데이터를 반환합니다.
if (redisTemplate.hasKey(key)) {
redisTemplate.expire(key, es, TimeUnit.SECONDS);
return redisTemplate.opsForValue().get(key);
}
//4. 이외의 경우 비지니스 코드를 실행합니다.
Object value = joinPoint.proceed();
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
return value;
}
private String generateKey(String cacheName, String key) {
return cacheName + "::" + key;
}
private String getDynamicValue(String[] parameterNames, Object[] args, String name) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return parser.parseExpression(name).getValue(context) + "";
}
}
테스트1 : 일반적인 리스트를 조회하는 경우
- name : 해당 데이터의 주체(이름)
- key : 데이터를 찾을 키를 명시하며 실제 레디스에 저장될 때에는 중복키가 발생될 수 있으므로 [name:::key] 와 같이 저장됨
- timeToLive : 캐시가 없는 경우 초기 설정 시간
- extensionSeconds : 캐시가 있는 경우 연장할 시간
@RedisCache(name = "productBank", key = "findAllV2", timeToLive = 60, extensionSeconds = 60)
public List<ProductBankDto.Response> findAllV2() {
List<ProductBank> items = productBankRepository.findAllFetchBranchOffice();
List<ProductBankDto.Response> responseList = items.stream().map(productBank -> {
ProductBankDto.Response response = productBank.toResponse();
response.setProductBankPartList(productBank.getBranchOfficeList().stream()
.filter(branchOffice -> ProductBankDto.BranchOfficeType.PART.equals(branchOffice.getBranchOfficeType()) && Boolean.TRUE.equals(branchOffice.getIsActive())).map(BranchOffice::toResponse).collect(Collectors.toList()));
response.setProductBankCenterList(productBank.getBranchOfficeList().stream()
.filter(branchOffice -> ProductBankDto.BranchOfficeType.CENTER.equals(branchOffice.getBranchOfficeType()) && Boolean.TRUE.equals(branchOffice.getIsActive())).map(BranchOffice::toResponse).collect(Collectors.toList()));
return response;
}).collect(Collectors.toList());
return responseList;
}
결과 비교
테스트2 : Dynamic ID 를 가진 경우(메서드 인자)
- name : 해당 데이터의 주체(이름)
- key : dynamic id(메서드 인자)
- timeToLive : 캐시가 없는 경우 초기 설정 시간
- extensionSeconds : 캐시가 있는 경우 연장할 시간
@RedisCache(name = "productBank", key = "#id", timeToLive = 180, extensionSeconds = 60)
public ProductBankDto.CmsResponse cmsGetBank(Long id) {
ProductBank productBank = productBankRepository.findByIdFetchJoin(id).orElseThrow(() -> new NotFoundException("productBank", true));
ProductBankDto.CmsResponse cmsResponse = productBank.toCmsResponse();
List<ProductBankBond> productBankBondList = productBankBondRepository.findByProductBankOrderByCodeCode(productBank);
cmsResponse.setProductBankBondList(
productBankBondList
.stream()
.map(ProductBankBond::toResponse)
.collect(Collectors.toList()));
Code code = codeRepository.findByCode(productBank.getType()).orElse(null);
if (code != null)
cmsResponse.setTypeInfo(code.toResponse());
List<ProductBankMailMasking> productBankMailMaskingList = productBankMailMaskingRepository.findByProductBank(productBank);
cmsResponse.setAllDept(productBankMailMaskingList.stream().map(ProductBankMailMasking::getCode).filter(vCode -> vCode.getCodeGroup().equals("MSKA")).map(Code::getCode).collect(Collectors.toList()));
cmsResponse.setJeonseDept(productBankMailMaskingList.stream().map(ProductBankMailMasking::getCode).filter(vCode -> vCode.getCodeGroup().equals("MSKJ")).map(Code::getCode).collect(Collectors.toList()));
if (productBank.getIsConsultationTime() == null) {
cmsResponse.setIsConsultationTime(false);
}
List<ConsultationTime> consultationTimeList = consultationTimeRepository.findAllByProductBankOrderByOrderNum(productBank);
if (consultationTimeList != null) {
cmsResponse.setConsultationTimeList(ConsultationTimeMapper.INSTANCE.listEntityToResponseDto(consultationTimeList));
}
ProductBankTemplate accountTemplate = productBankTemplateRepository.findFirstByProductBankIdAndCategoryCode(productBank.getId(), "ALCA001").orElse(null);
ProductBankTemplate bankTemplate = productBankTemplateRepository.findFirstByProductBankIdAndCategoryCode(productBank.getId(), "ALCA002").orElse(null);
if (accountTemplate != null) {
cmsResponse.setAccountTemplate(productBankTemplateService.getData(accountTemplate.getId()));
}
if (bankTemplate != null) {
cmsResponse.setBankTemplate(productBankTemplateService.getData(bankTemplate.getId()));
}
List<LoanCompareTester> loanCompareTesters = loanCompareTesterRepository.findAllByBank(productBank);
cmsResponse.setTesters(LoanCompareTesterDtoMapper.INSTANCE.toResponseList(loanCompareTesters));
return cmsResponse;
}
결과 비교
결론
적절하게 NoSQL을 사용하면 서버의 응답 속도와 데이터베이스 부하 모두를 잡을 수 있다.