Post

Redis 적용

Redis 적용

Redis

Java의 Redis Client

Java에서 Redis를 사용할 때 대표적으로 두 가지 Redis Client가 많이 사용된다.

  • Jedis

  • Lettuce


Jedis → 사용이 간단한 전통적인 동기 방식 Redis Client

Lettuce → Netty 기반의 비동기 처리로 고성능을 제공하는 Redis Client


관련 글
Jedis 보다 Lettuce 를 쓰자




2가지 접근 방식

Spring Data Redis는 Redis에 2가지 접근 방식을 제공한다.

RedisTemplate, Redis Repository를 이용한 방식이다.



공통 setting

공식문서


build.gradle

의존성 추가

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




application.properties

host 와 port 를 설정한다.

1
2
spring.redis.host = host.docker.internal
spring.redis.port = 6379

나는 docker에서 redis를 실행시켰기 때문에 host를 위와 같이 작성했다.

6379는 기본값이기 때문에 만약 Redis를 6379 로 띄웠다면 따로 설정하지 않아도 연결이 된다.


코드에서 properties에 작성한 값을 로딩할 때는

@Value("${spring.redis.port}")와 같이 SpEL(Spring Expression Language)을 작성하면 된다.




application

1
2
3
4
5
@SpringBootApplication
@EnableRedisHttpSession //Redis에 세션 데이터를 저장
public class AdmeApplication { // chat/room
   public static void main(String[] args) {
      SpringApplication.run(AdmeApplication.class, args);




Config

Spring Boot의 RedisAutoConfiguration은 RedisTemplate과 StringRedisTemplate 두 가지 bean을 자동으로 생성하여 제공하고 있다.

따라서 만약 개별 설정을 하고자 하는 경우 아래와 같이 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
public class RedisConfig {
   @Value("${spring.redis.host}")
   private String redisHost;
   @Value("${spring.redis.port}")
   private int redisPort;

/*
RedisTemplate 에 LettuceConnectionFactory 을 적용해주기 위해 설정
*/

   @Bean
   public RedisConnectionFactory redisConnectionFactory() {
      return new LettuceConnectionFactory(redisHost, redisPort);
   }

   @Bean //Redis 데이터에 쉽게 접근하기 위한 코드
   public RedisTemplate<?, ?> redisTemplate() { //RedisTemplate 에 LettuceConnectionFactory 을 적용
      RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
      redisTemplate.setConnectionFactory(redisConnectionFactory());
    
    //모든 Key, Value Serialization을 변경할 수 있음
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      redisTemplate.setValueSerializer(new StringRedisSerializer());
      return redisTemplate;
   }

   @Bean
   public StringRedisTemplate stringRedisTemplate() { //StringRedisTemplate은 문자열을 다룰 때 사용
      StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
      stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
      stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
      stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
      return stringRedisTemplate;
   }
}

StringRedisTemplate - docs.spring.io

RedisTemplate과 RedisTemplate의 문자열 중심 확장인 StringRedisTemplate 타입이 있다.

  • RedisTemplate의 defaultSerializer : JdkSerializationRedisSerializer
    • redisTemplate bean은 java Object를 redis에 저장하는 경우 사용
  • StringRedisTemplate의 defaultSerializer : StringRedisSerializer
    • stringRedisTemplate bean은 String 값을 key, value로 사용하는 경우 사용




public class StringRedisTemplate extends RedisTemplate<String,String>

StringRedisTemplate은 따로 Bean으로 안만들어도 쓸 수 있다.




RedisTemplate

Controller

1
2
3
4
5
6
7
8
9
10
11
12
    @MessageMapping("/chat/addUser")
    private final RedisService redisService;
    private final Long hours = 10L;
    
    public void addUser(@Payload ChatMessage chatMessage, @Header("Authorization") String token) {
        String nickname = jwtTokenProvider.getNickname(token);
        chatMessage.setSender(nickname);
        redisService.setRedisTemplate(chatMessage, hours);
        redisService.getRedisTemplate(chatMessage.getSender());
        chatService.connectUser("Connect", chatMessage.getRoomId());
        template.convertAndSend("/topic/public/" + chatMessage.getRoomId(), chatMessage);
    }




Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
@RequiredArgsConstructor //
public class RedisService {
    private final RedisTemplate<String, String> redisTemp;
    private final StringRedisTemplate redisTemplate;
    private final RedisRepository redisRepository;

    // string (opsForValue)
    public void setRedisTemplate(ChatMessage chatMessage, Long expirationTime){
        redisTemplate.opsForValue().set(chatMessage.getSender(), chatMessage.getRoomId(), expirationTime, TimeUnit.HOURS);
        // 키가 이미 있다면 마지막에 Set한 값으로 덮어씀
    }
  
    public String getRedisTemplate(String key){
        return redisTemplate.opsForValue().get(key);
    }
  
    public void setRedisValue(ChatMessage chatMessage){
        ValueOperations<String, String> values = redisTemp.opsForValue();
        values.set(chatMessage.getSender(), chatMessage.getRoomId());
    }
    public String getRedisValue(String key){
        ValueOperations<String, String> values = redisTemp.opsForValue();
        return values.get(key);
    }
}

String에 특화된 StringRedisTemplate이므로

String 만 다루려면 아래 서비스 사용하고, 그게 아니라면 RedisTemplate 빈 정의하여 사용하면 된다.


redisTemplate을 주입받은 후에 원하는 Key, Value 타입에 맞게 Operations 을 선언해서 사용할 수 있다.

ValueOperations, SetOperations, HashOperations은 각각 Strings, Set, Hash 자료구조에 대한 Operations이다.

ex) SetOperations<String, String> setOperations = redisTemp.opsForSet();


method설명
opsForValueStrings를 쉽게 Serialize / Deserialize 해주는 Interface
opsForListList를 쉽게 Serialize / Deserialize 해주는 Interface
opsForSetSet를 쉽게 Serialize / Deserialize 해주는 Interface
opsForZSetZSet를 쉽게 Serialize / Deserialize 해주는 Interface
opsForHashHash를 쉽게 Serialize / Deserialize 해주는 Interface




Redis Repository

Dto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RedisResponseDto {
   private String nickname;
   private String roomId;
   private Long hour;

   public static RedisResponseDto of(Redis redis){
      return RedisResponseDto.builder()
            .nickname(redis.getNickname())
            .roomId(redis.getRoomId())
            .hour(redis.getExpiration())
            .build();
   }
}




Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Getter
@RedisHash("chatRoom")
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Redis {
   @Id
   private String id;
   @Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션(findByAccessToken)
   private String nickname;
   private String roomId;
   @TimeToLive(unit = TimeUnit.HOURS)
   private Long expiration;
   @Builder
   public Redis(String nickname, String roomId, Long hour){
      this.nickname = nickname;
      this.roomId = roomId;
      this.expiration = hour;
   }
}




Repository

1
2
3
public interface RedisRepository extends CrudRepository<Redis, String> {
   Redis findByNickname(String nickname);
}




Service

1
2
3
4
5
6
7
8
9
10
11
12
public void addRedis(ChatMessage chatMessage, Long hours){
    Redis redis = new Redis(chatMessage.getSender(), chatMessage.getRoomId(), hours);
    Redis save = redisRepository.save(redis);
}
public String getRedis(String key){
    Redis byNickname = redisRepository.findByNickname(key);
    return RedisResponseDto.of(byNickname).getRoomId();
}

public void deleteRedis(String key){
    redisTemplate.delete(key);
}





Docker Redis 설치

*docker desktop 활용

image

redis를 run한 다음에

image

Optional settings에서 container name과 port를 작성했다.

그 다음에 application을 실행시키면 정상적으로 동작한다.




1
$ docker ps

docker에 띄워진 컨테이너 확인


CONTAINER ID 값 복사

1
docker exec -it {Redis컨테이너이름} redis-cli

docker로 redis 컨테이너를 실행


*-it란 무엇일까?

-it를 붙여줘야 명령어를 실행한 후 계속 명령어를 적을 수 있다.

만약 -it 가 없다면 redis-cli를 열어주기만 했다가 바로 다시 밖으로 나와버린다. 따라서 -it를 반드시 적어준다.

02. Docker 통한 Redis CLI 접속 방법 + 기본 사용법


1
$ keys *

redis에 저장된 key들을 확인


$ flushall : 전체 키 삭제

image




Redis 오류

1. org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; *nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost/:6379`*

처음엔 application.properties에 spring.redis.host = localhost 로 설정했다.

docker에 redis 설치후에 spring.redis.host = host.docker.internal로 변경했더니 실행이 잘 되었다.

도커에서 컨테이너를 띄우면 각 컨네이너와 localhost는 독립적이게 된다.

따라서 docker에서 실행중인 서버는 localhost로 접속을 하는 것이 아닌 docker host ip를 통해서 접속을 해야 한다.


redis 설치할 때 참고한 blog

window로 redis 설치 👉🏻 [REDIS] 📚 Window10 환경에 Redis 설치 & 설정
오류 해결 👉🏻 Docker Redis + Spring 연결



2. RedisConfig - bean이 2개

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   @Bean //Redis 데이터에 쉽게 접근하기 위한 코드
   public RedisTemplate<String, String> redisTemplate() { //RedisTemplate 에 LettuceConnectionFactory 을 적용
      RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
      redisTemplate.setConnectionFactory(redisConnectionFactory());
      redisTemplate.setKeySerializer(new StringRedisSerializer());
      redisTemplate.setValueSerializer(new StringRedisSerializer());
      return redisTemplate;
   }

   @Bean
   public StringRedisTemplate stringRedisTemplate() { //StringRedisTemplate은 문자열을 다룰 때 사용
      StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
      stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
      stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
      stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
      return stringRedisTemplate;
   }

StringRedisTemplate만 사용하려고 해서 굳이 bean등록을 안해도 되지만 한번 써보기 위해서 작성했다가 아래와 같이 에러가 떴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.dalcho.adme.service.RedisService required a single bean, but 2 were found:
   - redisTemplate: defined by method 'redisTemplate' in class path resource [com/dalcho/adme/config/RedisConfig.class]
   - stringRedisTemplate: defined by method 'stringRedisTemplate' in class path resource [com/dalcho/adme/config/RedisConfig.class]


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

내 생각에 해당 오류가 뜨는 이유는 redisTemplate도 String, String 타입인데

StringRedisTemplate은 RedisTemplate의 <String, String> 타입이기 때문에 같은 bean이 2개잖아? 이래서 뜨는 것 같다.

위 redsiTemplate의 타입을 <?,?>로 수정하면 해결 완료



3. redis 값

controller에서 save 후에 get을 하면 값이 떴는데 controller와 service를 이용해서 get을 받아오면 null이 떴다.

아래 코드를 추가시켜서 null이 뜨지 않았다.

1
implementation 'org.springframework.session:spring-session-data-redis'
1
2
3
@EnableRedisHttpSession //Redis에 세션 데이터를 저장
public class AdmeApplication {
}


findBy~할 때 model의 ~ 부분에 @Indexed를 붙여줘야 한다.

그래야 값을 가져올 수 있다.

1
2
@Indexed // 필드 값으로 데이터 찾을 수 있게 하는 어노테이션(findByAccessToken)
private String nickname;




reference
Spring Boot에서 Redis 활용
[Java + Redis] Spring Data Redis로 Redis와 연동하기 - RedisTemplate 편
Spring Boot Data Redis 사용해보기

This post is licensed under CC BY 4.0 by the author.