카테고리 없음

Redis 활용

issac 2024. 6. 19. 15:40

사용 기술

  • Redis(Bitnami)
  • Docker Compose
  • AOP

개선사항

  1. Refresh Token 을 기존 RDB 에서 NoSQL 로 구현하여 불필요한 부담을 줄인다.
  2. 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 에 동일하게 적재되어 있는지 확인한다.

Master 에서 test 라는 key 로 test-value 를 삽입한다.
get test

  • 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;
    }

 

결과 비교

초기 실행 시간 (178ms)
캐싱 이후 실행 시간 (82ms)
메모리 할당 확인

 

테스트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;
    }

결과 비교 

초기 실행시간 (1033ms)
캐싱 이후 실행 시간 (58ms)
메모리 할당 확인

 

결론

적절하게 NoSQL을 사용하면 서버의 응답 속도와 데이터베이스 부하 모두를 잡을 수 있다.