2 분 소요

현재 진행중인 프로젝트에서 DB에 영향을 주는 API를 굉장히 자주 호출해주는 경우가 있었는데, 이 횟수를 줄여보기 위해보자는 취지
에서 Redis 캐시를 활용해보고 싶었다. key-value로 저장하기 때문에 API 호출을 하면서 처리되는 작업 내용을 식별해줘야 했는데
다행히도 이 부분이 비교적 간단한 편이어서(=key로 표현하기가 쉬워서) 어느정도 구현이 수월했다.

Redis

설치 및 의존성 추가

Docker 컨테이너로 redis를 띄워서 연결했다.
여기를 참고!

application.yml

다음 내용을 추가해주자. Redis 설정에 필요한 host, port를 세팅해줘야 한다.

spring:
  cache:
    type: redis
  data:
    redis:
      host: 127.0.0.1
      port: 6379

RedisConfig

RedisTemplate를 빈으로 등록해서 다른 클래스에서도 주입받아 활용할 것이다!
이 때 직렬화/역직렬화를 위해 KeySerializer, ValueSerializer를 반드시 등록해줘야 한다.
설정해주지 않으면 SerializationException이 발생하니 주의하자.

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

Domain

Yoon

테스트용 엔티티 Yoon이다. 필드 count, subCount는 단순 카운팅을 위한 변수다.

public class Yoon {

    @Id @GeneratedValue
    @Column(name = "yoon_id")
    private Long id;
    private Integer count;
    private Integer subCount;

    public void addCount(Integer value) {
        count += value;
    }

    public void addSubCount(Integer value) {
        subCount += value;
    }
}

YoonRepository

스프링 데이터 JPA로 Yoon 엔티티를 관리할 수 있도록 하는 클래스 YoonRepository를 만들어주자.

public interface YoonRepository extends JpaRepository<Yoon, Long> {
}

테스트

RedisService

redisTemplate을 통해서 redis 캐시에 접근할 수 있다. 여기서는 카운팅을 위해 increment()를 사용했으나 바로 저장하고
싶다면 set(String key, Object value)를 사용하면 된다.

@Service
@RequiredArgsConstructor
public class RedisService {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final YoonRepository yoonRepository;

    //value 1 증가 (key : count_{id})
    public void incrementCount(Integer id) {
        redisTemplate.opsForValue().increment("count_" + id);
    }

    //value 1 증가 (key : subCount_{id})
    public void incrementSubCount(Integer id) {
        redisTemplate.opsForValue().increment("subCount_" + id);
    }

    /*
      <key, value>
      - {count_1, 50}
      - {count_2, 30}
      - {subCount_1, 40}
    */
    public void saveTest() {
        for(int i=0; i<50; i++) {
            incrementCount(1);
        }

        for(int i=0; i<30; i++) {
            incrementCount(2);
        }

        for(int i=0; i<40; i++) {
            incrementSubCount(1);
        }
    }

    /*
      Scheduler에 의해 주기적으로 호출될 함수로, id, count를 파싱해서 업데이트
      count_, subCount_로 시작하는 key를 모두 불러와서 처리
      처리 후에는 redis 캐시에서 <id, count> 삭제
    */
    @Transactional
    public void schedulerTest() {
        redisTemplate.keys("count_*").forEach(key -> {
            Long id = Long.parseLong(key.split("_")[1]);
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(key).toString());

            yoonRepository.findById(id).ifPresent(yoon -> {
                yoon.addCount(count); //count 갱신
            });

            redisTemplate.delete(key);
        });

        redisTemplate.keys("subCount_*").forEach(key -> {
            Long id = Long.parseLong(key.split("_")[1]);
            Integer count = Integer.parseInt(redisTemplate.opsForValue().get(key).toString());

            yoonRepository.findById(id).ifPresent(yoon -> {
                yoon.addTimeCount(count); //subCount 갱신
            });

            redisTemplate.delete(key);
        });
    }
}
  • Yoon 엔티티에서 업데이트된 count, subCount는 변경감지때문에 트랜잭션이 끝나면 DB에 반영됨

TestController

localhost:8080/testGET 요청을 보내면 saveTest()를 호출하면서 redis 캐시에 key-value를 저장할 것이다!

@RestController
@RequiredArgsConstructor
public class TestController {

    private final RedisService redisService;

    @GetMapping("/test")
    public String test() {
        redisService.saveTest();
        return "test function is called. check redis.";
    }
}

실제로 저장됐는지 확인하고 싶으면 (docker 컨테이너로 redis를 띄웠다면) 다음 명령어를 입력해보자.

# redis 컨테이너 접속
> docker exec -it {redis_container_id} /bin/sh

# redis-cli 실행
> redis cli

# key 목록 조회
> keys *

# value 조회
> get {key 정보}

CountScheduler

위에서 schedulerTest()를 scheduler에 의해 주기적으로 호출된다고 설명했다. 아래는 이를 담당하는 CountScheduler이다.
@Scheduled에 1분을 주기로 설정했으니 test()를 1분마다 호출할 것이다.
그러면 schedulerTest()에 의해 redis 캐시에 등록된 값들을 DB에 반영해주고 초기화하는 작업을 진행한다.

@Component
@RequiredArgsConstructor
public class CountScheduler {

    private final ProductService productService;
    private final RedisService redisService;
    private final int minute = 1000 * 60;

    @Scheduled(fixedDelay = minute)
    public void test() {
        redisService.schedulerTest();
    }
}

서버를 실행하면 1분마다 test()가 호출되면서 여러 작업을 수행하고 그 결과가 반영된걸 DB에서 확인해볼 수 있을 것이다.
로그에도 쿼리가 날라가니 꼭 확인해보자!

Reference

카테고리:

업데이트:

댓글남기기