본문 바로가기

리눅스 인프라/Redis

DB 접근을 줄이고 빠른 조회를 위한 Spring Redis 사용

관련 내용

해당프로젝트깃허브

커밋시점

[리눅스 인프라/Redis] - Gloabal Cache Redis에 대한 이해

개요 목적

DB에서 조회하는 데이터가 변경은 적고 자주 사용된다면, Redis에 저장하여, 캐시하는 것이 조회 시간을 단축하고, DB 부하를 줄이는 좋은 방법이다. Spring과 Redis를 연결해서 음식 목록을 빠르게 조회하는 방법에 대해서 알아보겠다.

원하는 상황

아래 그림처럼 주문 페이지로 이동하면, 분식집에서 팔고 있는 음식 목록들을 보여준다. 음식점 별 음식 목록을 DB가 아닌 Redis에서 빠르게 조회하는 것이 이번 글의 목표이다.

만약 Redis에서 음식 목록이 없다고 응답하면, 다시 DB에서 음식 목록을 요청한다.

개발 환경

  • SpringBoot(gradle) - 2.7.5
  • Ubuntu - 20.04

Redis 이해하기

[리눅스 인프라/Redis] - Gloabal Cache Redis에 대한 이해

Ubuntu - Redis 다운 작동 확인

다운

//다운로드
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install redis-server

//다운로드 확인하기
redis-server --version

실행

//실행
sudo systemctl start redis-server

//기본 6379포트를 잘 사용하고 있는 지 확인
netstat -nlpt | grep 6379

Spring Data Redis 사용하기

Spring Data Redis 설정

Spring Data Redis 의존성 주입

SeperateDeliveryService\APIDeliveryService\build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Redis Yaml 파일 설정

SeperateDeliveryService\APIDeliveryService\src\main\resources\application.yml

spring:
  redis:
    host: 127.0.0.3
    port: 6379

@EnableCaching 적용

@SpringBootApplication
@EnableCaching
public class ApiDeliveryServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiDeliveryServiceApplication.class, args);
    }
}

@EnableCaching을 적용하게 되면 @Cacheable 어노테이션이 붙은 메서드가 실행될 때 내부적으로 Proxy, AspectJ 기반 어드바이스를 CacheInterceptor와 연결하여 Spring에서 캐시 관리에 필요한 구성 요소를 등록한다.

RedistTemplate 빈으로 등록

SeperateDeliveryService\APIDeliveryService\src\main\java\com\example\apideliveryservice\webconfig\RedisConfig.java

@Configuration
public class RedisConfig  {
    
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private int redisPort;
    
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }
    
    /*
        RedisTemplate은 주어진 객체를 직렬화, 역직렬화하여 Redis에 저장한다.
        setSerializer를 통해서 직렬화 방식을 설정 할 수 있다.
    */
    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
    
    /*
        Redis Cache 적용을 위한 RedisCacheManager 설정
     */
    @Bean
    public CacheManager redisCacheManager() {
        RedisCacheConfiguration redisCacheConfiguration
            = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(
                new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                new GenericJackson2JsonRedisSerializer()));

        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
            .fromConnectionFactory(connectionFactory())
            .cacheDefaults(redisCacheConfiguration)
            .build();

        return redisCacheManager;
    }
}

(핵심) 비지니스 메소드 Redis 적용하기

빈으로 등록한 RedisTemplate을 사용하여, 음식 목록을 가져오고, 음식 가격이 수정되면 Redis에 있는 음식 목록을 삭제하는 코드에 대해서 알아보자.

음식 목록 가져오기

SeperateDeliveryService\APIDeliveryService\src\main\java\com\example\apideliveryservice\service\CompanyFoodService.java

//적용 전
@Service
@Slf4j
@RequiredArgsConstructor
public class CompanyFoodService {

    private final CompanyFoodRepository companyFoodRepository;
    private final CompanyMemberRepository companyMemberRepository;
    
    @Transactional(readOnly = true)
    public List<CompanyFoodDto> findAllFoodByCompanyMemberId(Long companyMemberId) {
        CompanyMemberEntity findCompanyMember = companyMemberRepository.findById(companyMemberId)
            .orElseThrow();
        List<CompanyFoodEntity> findCompanyFoodList =
            companyFoodRepository.findAllByCompanyMemberEntity(findCompanyMember);
        List<CompanyFoodDto> companyFoodDtoList = changeFoodEntityListToDto(findCompanyFoodList);
        return companyFoodDtoList;
    }
}

//적용 후
@Service
@Slf4j
@RequiredArgsConstructor
public class CompanyFoodService {

    private final CompanyFoodRepository companyFoodRepository;
    private final CompanyMemberRepository companyMemberRepository;
    private final RedisTemplate<String, List<CompanyFoodDto>> redisTemplate;

    @Transactional(readOnly = true)
    public List<CompanyFoodDto> findAllFoodByCompanyMemberId(Long companyMemberId) {
        //redis
        //음식 목록을 가져올 때, 먼저 Redis에 해당 음식점 음식 목록이 있는 지 조회한다.
        //존재 한다면 redis에서 가져온 목록을 전달한다.
        List<CompanyFoodDto> redisValue = redisTemplate.opsForValue()
            .get("companyFood::" + companyMemberId);
        if (redisValue != null) {
            return redisValue;
        } else {
            CompanyMemberEntity findCompanyMember = companyMemberRepository.findById(companyMemberId)
                .orElseThrow();
            List<CompanyFoodEntity> findCompanyFoodList =
                companyFoodRepository.findAllByCompanyMemberEntity(findCompanyMember);
            List<CompanyFoodDto> companyFoodDtoList = changeFoodEntityListToDto(findCompanyFoodList);
            //redis에서 해당 음식 목록이 존재하지 않으면 DB에서 조회한 후 
            //그 값을 레디스에 저장한다.
            redisTemplate.opsForValue().set("companyFood::" + companyMemberId, companyFoodDtoList);
            return companyFoodDtoList;
        }
    }
}

음식 가격 업데이트 시 기존 Redis에 있던 음식 목록 삭제하기.

SeperateDeliveryService\APIDeliveryService\src\main\java\com\example\apideliveryservice\service\CompanyFoodService.java

@Service
@Slf4j
@RequiredArgsConstructor
public class CompanyFoodService {

    private final RedisTemplate<String, List<CompanyFoodDto>> redisTemplate;
		@Transactional
	    public void updateFoodPrice(Long companyFoodId, BigDecimal updatePrice) {
	        try {
	            CompanyFoodEntity findCompanyFood = companyFoodPriceUpdate(companyFoodId, updatePrice);
	            saveCompanyFoodUpdatePriceHistory(updatePrice, findCompanyFood);
	            //음식 가격 변경이 되면 기존 Redis에 있는 음식 목록을 삭제한다.
	            redisTemplate.delete(
	                "companyFood::" + findCompanyFood.getCompanyMemberEntity().getId());
	        } catch (DataIntegrityViolationException e) {
	            throw new CompanyFoodException(UPDATE_PRICE_REQUEST_PRICE_BLANK.getErrormessage());
	        }
	    }
}

음식 목록에 대한 정보가 변하기 때문에, Redis의 데이터를 삭제하거나, 업데이트 된 데이터를 전달해야 한다. 그래서 Redis는 변경이 적고, 이용을 많이 하는 데이터를 캐시하기 좋다.

추가 @EnableCaching, CacheManager빈을 사용한 @Cacheable 사용 방법

@EnableCaching 어노테이션과 CacheManager빈을 등록한 설정은 @Cacheable을 사용하기 위함이다. RedisTemplate이 아닌 @Cacheable 어노테이션을 사용한 Redis 사용 방법에 대해서 간단하게 알아보자.

@Cacheable 사용

SeperateDeliveryService\APIDeliveryService\src\main\java\com\example\apideliveryservice\service\CompanyMemberService.java

@Service
@RequiredArgsConstructor
public class CompanyMemberService {

    private final CompanyMemberRepository companyMemberRepository;

    @Transactional(readOnly = true)
    @Cacheable(value = "companyMemberList")
    public List<CompanyMemberDto> findAllMember(){
        List<CompanyMemberEntity> allCompanyMember = companyMemberRepository.findAll();
        return changeMemberEntityListToDtoList(
            allCompanyMember);
    }
}

해당 메소드는 음식점 주인들의 목록을 가져오는 기능을 한다.

@Caheable value에 Redis에 저장한 key값을 작성하여, 음식점 사장님 목록들을 가져 올 수 있다.

캐시에 값이 없다면, 해당 로직을 실행하여 목록들을 가져온다.

@CacheEvict 사용

SeperateDeliveryService\APIDeliveryService\src\main\java\com\example\apideliveryservice\service\CompanyMemberService.java

@Service
@RequiredArgsConstructor
public class CompanyMemberService {

    private final CompanyMemberRepository companyMemberRepository;
    @Transactional
    @CacheEvict(value = "companyMemberList", allEntries = true)
    public void join(String loginName, String password, String name){
        validateDuplicateLoginName(loginName);
        CompanyMemberEntity companyMemberEntity = getCompanyMemberEntity(loginName, password, name);
        companyMemberRepository.save(companyMemberEntity);
    }
}

@CacheEvict에 캐시 이름을 넣어주면 메소드가 실행될 때 캐시의 내용이 제거된다.

새로운 음식점 사장님을 등록하는 메소드가 실행하면, 캐시에 있는 음식점 사장님 목록을 제거한다.

allEntires = true 설정을 통해 캐시에 저장된 값을 모두 제거한다.

결과 확인하기

음식 목록과 음식점 사장님 목록을 조회 하였더니, Redis 서버에 해당 내용들이 저장된 것을 볼 수 있다.

Spring Redis 적용 시 주의 사항

  • 레디스 메모리 성능을 주의하여 키 값을 최대한 짧게 유지한다. companyMemberList::simpleList[] → CML이렇게 줄여서 저장해 최대한 레디스에서 값을 빨리 찾을 수 있게 키 값을 저장한다.
  • 함수나 Enum을 t지정해서 설정 값을 일관성 있게 사용한다.
  • Test할 때는 emebeded가 아닌, mock 사용해서 테스트를 진행한다.

Reference

https://hayden-archive.tistory.com/429

https://jane096.github.io/project/redis-caching-part2/

https://velog.io/@bey1548/스프링-캐시Cacheable-CacheEvict

'리눅스 인프라 > Redis' 카테고리의 다른 글

Gloabal Cache Redis에 대한 이해  (0) 2023.04.05