Redis Cache를 사용하기 위해선 먼저 Redis를 사용할 줄 알아야 합니다. 그래서 하나씩 만들어보겠습니다. 

Redis는 Nosql DB입니다. 영속성을 지원하며, In memory DB이므로 속도가 빠른 장점으로 Cache서버의 DB로서 많이 사용됩니다. 

 

1. 디펜던시 추가

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

build.gradle에 위의 디펜더 시를 추가해줍니다.

2. RedisConfig

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories
public class RedisConfig {
    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        return new LettuceConnectionFactory(redisProperties.getHost(),redisProperties.getPort());
    }

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

2-1 RedisConnectionFactory

- Java에서 지원하는 RedisClient는 2가지가 있습니다. (Jedis, Lettuce)

- Jedis는 멀티쓰레드에서 불안정하고, Pool의 한계 등으로 현재 Springboot 2.0 이상에서는 Lettuce가 탑재되고, 사용된다. 

- RedisProperties는 Redis의 기본 설정들을 가지고 있습니다. 

- RedisProperties를 이용해서 Redis의 Host와 Port를 설정해줍니다. 

LettuceConnectionFactory에 설정을 부여해서, 클러스터에 대한 설정도 할 수 있지만 여기서는 진행하지 않겠습니다. 

2-2 RedisTemplate

Redis에 데이터를 저장하는 방법은 총 2가지 있습니다.

1. CrudRepository 상속받아 사용

- Redis를 사용할 Repository를 만들고 CrudRepository를 상속받아 사용합니다. 

2. RedisTemplate 정의해서 사용

- Redis에 직렬화 설정을 부여해서 저장하는데, 자동으로 직렬화를 수행해줍니다. 

ConnectionFactory를 Luttece를 설정해줍니다. 

- setKeySerializer(new StringRedisSerializer()): String으로 들어오는 key에 대한 직렬화를 수행합니다. 즉 key에 대한 직렬화 설정을 해주는 메서드입니다. 

- setValueSerializer(new GenericJackson2JsonRedisSerializer)): 저장하려 하는  Object객체를 직렬화 하여 저장합니다. 즉 Value에 대한 직렬화 설정을 하주는 메서드입니다. 

- RedisTemplate<String,?>: key값은 항상 String이므로 String, Value는 와일드카드를 사용해서, 다양한 객체를 저장할 수 있도록 해줍니다. 

 

2022.04.29 추가


sprigboot 2.0 이상부터는 config 작성없이 application.properties나 yml로 작성이 가능합니다. 

spring:
  redis:
    host: localhost
    port: 6379

3. CrudRepository 사용

CrudRepository를 상속받아서 Redis를 사용해보겠습니다.

3-1. RedisMember class

@Getter
@NoArgsConstructor
@RedisHash(value = "redisMember")
public class RedisMember{

    @Id
    private String id;
    private String name;
    private int age;

    public RedisMember(String name, int age) {
        this.id= name;
        this.name = name;
        this.age = age;
    }

}

- @Id는 javaPersistent의 어노테이션입니다. 

- @RedisHash: Redis에 저장될 때 사용되는 prefix입니다. ex) redisMember:key 사용됩니다.

-  Id가 Key값으로 사용됩니다. 저희는 name을 key값으로 사용할 예정이므로 Id에도 key를 넣어줍니다. 만약 안 넣어줄 시 Redis에 입력될 때 랜덤 한 값으로 key값이 생성됩니다.

첫 번째는 id를 미지정, 두 번째는 id에 name으로 줬을 때입니다. 
prefix로 redisMember가 들어간 것을 볼 수 있습니다. 

3-2. MemberRedisRepository

public interface MemberRedisRepository extends CrudRepository<RedisMember,String> {

}

- CrudRepository<>를 상속받습니다. 

- Id 값을 이용하여 데이터를 찾습니다. 

간단한 테스트 코드를 보겠습니다. 

@Test
@DisplayName("redisRepository 테스트")
void repositoryTest(){

    memberRedisRepository.save(new RedisMember("hi",10));

    RedisMember redisMember = memberRedisRepository.findById("hi").get();
    
    assertThat(redisMember.getName()).isEqualTo("hi");

}

- RedisMember id,name= hi이며, age=10인 객체를 저장합니다. 

- redis가 만약 설치되어 있다면, redis-cli를 입력하고 위처럼 입력한다면 값이 저장된 것을 확인할 수 있습니다. 

- Id를 가지고 데이터를 찾으므로 값을 변경하고 싶다면, 다시 저장하시면 됩니다. 

@Test
@DisplayName("redisRepository 데이터변경 테스트")
void repositoryUpdateTest(){

    memberRedisRepository.save(new RedisMember("hi",10));

    memberRedisRepository.save(new RedisMember("hi",11));

    RedisMember redisMember = memberRedisRepository.findById("hi").get();

    assertThat(redisMember.getAge()).isEqualTo(11);

}

- 삭제는 delete를 이용하면 됩니다. 

 

전체를 반환하는 테스트를 해보겠습니다. JPA와 다르게 Iterable <> 형태로 반환됩니다. 

@Test
@DisplayName("redisRepository 전체 반환 테스트")
void repositoryFindAllTest(){
    memberRedisRepository.save(new RedisMember("hi1",10));

    memberRedisRepository.save(new RedisMember("hi2",11));


    Iterable<RedisMember> all = memberRedisRepository.findAll();

    List<RedisMember> list = new ArrayList<>();

    all.forEach(list::add);

    assertThat(list.size()).isEqualTo(2);

}

- forEach의 람다 표현식으로 List에 담아서 사용합니다. 

4. RedisTemplate 사용

위에서 선언한 RedisTemplate를 사용합니다.

 

먼저 사용가능한 자료구조와 사용하기 위한 객체를 살펴보겠습니다.

   메소드명반환              오퍼레이션            Redis 자료구조

opsForValue() ValueOperations String
opsForList() ListOperations List
opsForSet() SetOperations Set
opsForZSet() ZSetOperations Sorted Set
opsForHash() HashOperations Hash

Operations연산자를 얻어서 연산을 수행합니다. 

4-1. ValueOperation

Value는 String에 대해서 연산을 진행합니다.

@Autowired
RedisTemplate<String, RedisMember> redisMemberRedisTemplate;

@Test
@DisplayName("redisTemplate value 테스트")
void redisTemplateTest(){
    ValueOperations<String, RedisMember> stringRedisMemberValueOperations = redisMemberRedisTemplate.opsForValue();

    stringRedisMemberValueOperations.set("hi", new RedisMember("hi", 10));

    RedisMember redisMember = redisMemberRedisTemplate.opsForValue().get("hi");

    assertThat(redisMember.getAge()).isEqualTo(10);

}

- 연산자를 먼저 얻습니다. 그 후 set을 이용해서 객체를 저장합니다. 

- 객체를 찾을 때 지정한 key를 이용해서 데이터를 찾습니다. 

- 여기서 알아야할 부분은 원래 Value는 String에 대해서 연산을 진행하지만, RedisConfig에서 객체에 대한 직렬화 설정을 부여했기 때문에 자동적으로 직렬화와 역직렬 화가 수행돼, RedisMember를 저장하고 데이터를 찾을 수 있습니다.

- 마찬가지로 업데이트 연산은 set(key, 변경)을 이용하면 됩니다. 

4-2 ListOperation

List에 대한 연산을 진행합니다.

@Test
@DisplayName("redisTemplate list 테스트")
void redisTemplateListTest(){
    ListOperations<String, RedisMember> stringRedisMemberListOperations = redisMemberRedisTemplate.opsForList();

    stringRedisMemberListOperations.rightPush("redisMemberList",new RedisMember("hi1",10));

    stringRedisMemberListOperations.rightPush("redisMemberList",new RedisMember("hi2",11));

    Long size = stringRedisMemberListOperations.size("redisMemberList");

    RedisMember result = stringRedisMemberListOperations.index("redisMemberList", 1);
    
    List<RedisMember> redisMemberList = stringRedisMemberListOperations.range("redisMemberList", 0, 1);
	        
    stringRedisMemberListOperations.set("redisMemberList",0,new RedisMember("m1",10));


}

- rightPush: List에 추가합니다. 

- size: 크기를 알 수 있습니다.

- index: 단 건의 데이터를 조회할 수 있습니다. 

- range: 다수의 데이터를 조회할 수 있습니다. 

- set: 데이터의 값을 변경할 수 있습니다. 

4-3 SetOperation

set에 대한 연산을 진행합니다.

@Test
@DisplayName("redisTemplate set 테스트")
void redisTemplateSetTest(){
    SetOperations<String, RedisMember> stringRedisMemberSetOperations = redisMemberRedisTemplate.opsForSet();

    stringRedisMemberSetOperations.add("memberSet",new RedisMember("h1",10));
    stringRedisMemberSetOperations.add("memberSet",new RedisMember("h2",10));

    RedisMember randomRedisMember = stringRedisMemberSetOperations.pop("memberSet");

    List<RedisMember> randomMemberSet = stringRedisMemberSetOperations.pop("memberSet", 2);

    Set<RedisMember> memberSet = stringRedisMemberSetOperations.members("memberSet");

    Boolean result = stringRedisMemberSetOperations.isMember("memberSet", new RedisMember("h1", 10));

    Long re = stringRedisMemberSetOperations.remove("memberSet", new RedisMember("hi2", 10));
    
}

- add: set에 추가할 수 있습니다.

- pop(key): set에 저장된 데이터중에서 랜덤 하게 한 개의 데이터를 반환합니다.

- pop(key, count): set에 저장된 데이터중에서 랜덤 하게 count만큼의 데이터를 반환합니다. 

- member(key): set에 저장된 모든 데이터를 반환합니다.

- isMember(key, 데이터): 데이터의 존재 유무를 판별합니다.

- remove(key,데이터): 데이터에 관한 삭제를 진행하며, 몇 건의 데이터가 삭제됐는지 반환됩니다. 

4-4. ZSetOperation

위에서 본 set과는 다르게 값에 대해 가중치를 주어 정렬을 진행합니다.

@Test
@DisplayName("redisTemplate zSet 테스트")
void redisTemplateZSetTest(){
    ZSetOperations<String, RedisMember> stringRedisMemberZSetOperations = redisMemberRedisTemplate.opsForZSet();
    stringRedisMemberZSetOperations.add("memberZSet",new RedisMember("hi1",10),1);
    stringRedisMemberZSetOperations.add("memberZSet",new RedisMember("hi2",10),2);

    Long memberZSet = stringRedisMemberZSetOperations.count("memberZSet", 0, 1);

    RedisMember result = stringRedisMemberZSetOperations.popMin("memberZSet").getValue();
}

- add(key, value, 가중치): 가중치를 기준으로 value에 대한 정렬을 진행합니다.

- count(key, min, max): min~max까지의 개수를 반환합니다. 

- popMin(key).getValue(): 가장 작은 가중치를 가진 데이터를 반환합니다.

4-5. HashOperation

hash를 이용하여 key과 hashKey값을 통해서 데이터를 저장, 조회합니다.

hash를 사용하기 위해선 config에 hash key값과 value값을 직렬화 해주는 과정이 필요합니다.

아래와 같이 추가해줍니다. 

@Bean
public RedisTemplate<String,?> redisTemplate(){
    RedisTemplate<String, ?> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setKeySerializer(new StringRedisSerializer()); // 추가
    redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); // 추가
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return redisTemplate;
}
@Test
@DisplayName("redisTemplate hash 테스트")
void redisTemplateHashTest(){

    HashOperations<String, Object, Object> stringObjectObjectHashOperations =
            redisMemberRedisTemplate.opsForHash();
    stringObjectObjectHashOperations.put("memberHashOne","name",new RedisMember("hi3",10));

    Map<String, RedisMember> map = new HashMap<>();

    map.put("hi1",new RedisMember("hi1", 10));
    map.put("hi2",new RedisMember("hi2", 10));

    stringObjectObjectHashOperations.putAll("memberHashMap",map);

    RedisMember result = (RedisMember) stringObjectObjectHashOperations.get("memberHashMap", "hi1");
	 RedisMember redisMember = (RedisMember) stringObjectObjectHashOperations.get("memberHashOne", "name");
}

- put(key, hashKey, value): hashKey값을 이용해서 데이터를 저장합니다. 

- putAll(key, Map): map의 key값이 hashKey값으로 들어가게 됩니다.

- get(key, hashKey): key값과 hashKey값을 이용해서 데이터를 찾습니다.

정리하기 

Redis 디펜더시를 추가하여 사용할 수 있고, RedisConfig를 작성

- RedisConnectionFactory를 지정하고, properties를 주입해준다.

- RedisTemplate: 연산을 위한 Template

 

Redis에 연산 진행 2가지 방법

1. CrudRepository 상속

- @RedisHash는 prefix를 지정한다.

- Id가 key값으로 들어가게 된다. 미지정 시 랜덤 한 값으로 

2. RedisTemplate 사용

- connectionFactory를 설정

- key와 value에 대한 직렬화 방식을 설정해줘 자동으로 직렬화와 역직렬화가 진행

- Value, List, Set, ZSet, Hash에 대한 연산 방법

 

다음은 Redis의 사용방법을 활용해서 Cache를 적용해보겠습니다. 읽어주셔서 감사합니다.

 

모든 코드는 아래 링크에서 확인 가능합니다.

https://github.com/rlaehdals/springbootCache