Spring/Spring Boot

Spring 에서 Redis 사용하기 (설정, In-memory DB, Transaction)

민돌v 2024. 7. 1. 15:28
본 포스팅은 다음과 같은 환경 아래에서 진행합니다. JDK 17, Spring Boot 3.x, Gradle 

💡Redis 묶음

 

이번 포스팅에서는 Spring 에서 제공하는 Spring Data Redis 를 사용하여 Redis 를 이용하는 방법에 대해 기록하고자 합니다.
대부분의 내용이 Spring Data Redis 공식문서 내용 정리입니다.

 


[목차]

  1. 의존성추가
  2. Redis 연결
    1. Redis Connection
    2. RedisTemplate
      1. RedisTemplate
      2. StringRedisTemplate
    3. Redis Repository
  3. Redis Transaction
    1. Spring Data Redis Transaction 동작과정
    2. Redis Transaction 파이프라인 직접 구현

 

 


1. 의존성 추가

//redis
implementation 'org.springframework.data:spring-data-redis'

참고 : https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis/3.3.1

 


2. Redis 연결

  • Redis와 Spring을 사용할 때 가장 먼저 해야 할 작업 중 하나는 IoC 컨테이너를 통해 스토어에 연결하는 것입니다.
  • Spring 공식문서에서 말하기를 RedisConnectionRedisConnectionFactory 인터페이스만 사용해야한다고 합니다.
  • RedisConnection 객체는 RedisConnectionFactory를 통해 생성할 수 있습니다.
  • RedisConnection 클래스는 thread-safety 하지 않다고 합니다.
    → 기본 네이티브 연결(예: Lettuce의 StatefulRedisConnection)은 스레드에 안전할 수 있지만, Spring Data Redis의 LettuceConnection 클래스 자체는 그렇지 않습니다.
  • 💡 따라서 Thread Safe 한 작업을 위해 권장하는 방법은, RedisTemplate 를 사용하는 것이라고 합니다.

 

[공식문서 발췌]
If you need to share (stateful) Redis resources, like connections, across multiple Threads, for performance reasons or otherwise, then you should acquire the native connection and use the Redis client library (driver) API directly. Alternatively, you can use theRedisTemplate, which acquires and manages connections for operations (and Redis commands) in a Thread-safe manner. See documentation on RedisTemplate for more details.


1) Redis Connection 얻어오기

자바의 Redis Client 라이브러리는 Jedis와 Lettuce가 있습니다.
Spring 공식문서 자체에서 Lettuce 에 대한 언급이 많으므로 저는 Lettuce 를 사용해보았습니다.

Lettuce 는 패키지를 통해 Spring Data Redis가 지원하는 Netty 기반 오픈 소스 커넥터 입니다.

@Configuration
public class RedisConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        return new LettuceConnectionFactory(new RedisStandaloneConfiguration("server", 6379));
    }
}

gradle.build

  • lettuce 의존성 추가
//redis
implementation 'org.springframework.data:spring-data-redis'
implementation 'io.lettuce:lettuce-core'

+ (참고 내용) Spring Data Redis 에서는 Redis 서버의 고가용성을 위해 Redis Sentinal 을 제공합니다.

예시 - 참고(링크)

/**
 * Lettuce
 */
@Bean
public RedisConnectionFactory lettuceConnectionFactory() {
  RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
  .master("mymaster")
  .sentinel("127.0.0.1", 26379)
  .sentinel("127.0.0.1", 26380);
  return new LettuceConnectionFactory(sentinelConfig);
}

/**
 * Jedis
 */
@Bean
public RedisConnectionFactory jedisConnectionFactory() {
  RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
  .master("mymaster")
  .sentinel("127.0.0.1", 26379)
  .sentinel("127.0.0.1", 26380);
  return new JedisConnectionFactory(sentinelConfig);
}

2) RedisTemplate 사용하기

RedisConfig.class

  • RedisTemplate
    • RedisTemplate 는 한번 구성되면 스레드에 안전합니다.
    • 매겨변수 <k, v>를 가집니다.
      • K : 템플릿이 작동하는 Redis 키 유형(일반적으로 문자열)
      • V : 템플릿이 작동하는 Redis 값 유형
    • 👏 프레임워크의 관점에서,Redis에 저장되는 데이터는 byte[]로 직렬화하기 때문에
      Redis 저장시 "key" 와 "value"의 데이터를 문자열로 지정하기위해 직렬화 도구(StringRedisSerializer)를 별도로 지정해 주어야합니다. 
  •   StringRedisTemplate
    • 보통 Redis Key에 저장되는 값의 타입은 '문자열'이 대부분이기 때문에 집중적인 String 연산을 위한 편리한 원스톱 솔루션으로StringRedisConnection(및 그 DefaultStringRedisConnection 구현)과 StringRedisTemplate이라는 두 가지 확장을 제공한다고 합니다.
    • 템플릿과 연결은 String 키에 바인딩될 뿐만 아니라 그 아래에 StringRedisSerializer를 사용하므로 저장된 키와 값을 사람이 읽을 수 있습니다(Redis와 코드에서 동일한 인코딩이 사용된다고 가정할 때)
@Configuration
public class RedisConfig {
	
	//민감정보 외부 주입을 위한 host, port 변수	
    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {

        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }

    @Bean
    RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        
        return template;
    }
    
    @Bean
    StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {

        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

→ 결론적으로 위와같이 설정했을 때 RedisTemplateStringRedisTemplate는 동일한 인코딩 결과를 가져옵니다.
→ 선호에 따라 선택해서 사용하면.. 되겠져?

 

✔️ RedisTemplate 예시

1. 레디스 템플릿은 사용하는 자료구조마다 제공하는 메서드가 다르기 때문에 사용하고자 하는 데이터 타입 객체를 만들어 레디스의 자료구조 타입에 맞는 메소드를 사용할 수 있습니다.

메서드 명 레디스 타입
opsForValue String
opsForList List
opsForSet Set
opsForZSet Sorted Set
opsForHash Hash
@Repository
@RequiredArgsConstructor
public class RedisRepository {

    private final RedisTemplate<String, String> redisTemplate;

    public void forList() {
        redisTemplate.opsForList().leftPush("forList", "1");
    }

    public void forSet() {
        redisTemplate.opsForSet().add("forSet", "1");
    }

    public void forString() {
        redisTemplate.opsForValue().increment("forString", 1);
    }
}

 

2. 혹은 특정 템플릿을 종속성으로 선언하고 템플릿을 삽입하여 사용하여 다음 예제와 같이 컨테이너가 자동으로 변환을 수행하여 opsFor[X] 호출을 제거할 수 있습니다.

@Repository
@RequiredArgsConstructor
public class RedisRepository {

    private final RedisTemplate<String, String> redisTemplate;
    
    // inject the template as ListOperations
    @Resource(name="redisTemplate")
    private ListOperations<String, String> listOps;

    public void forList() {
        listOps.leftPush("forList", "1");
    }
}

 

✨ 레디스 명령어 모음 → https://redis.io/docs/latest/commands/


3) Redis Repository

Redis Repository를 사용하여 도메인 객체를 Redis Key에 매핑하는 방법도 존재합니다.
Redis Repository는 사용하면, Redis 해시에서 도메인 개체를 변환 및 저장하고, 사용자 지정 매핑 전략을 적용하고, 보조 인덱스를 사용할 수 있습니다.

✔️ RedisRepository 는 Redis Hash 자료구조를 사용합니다.

  • 따라서 @RedisHash 어노테이션과 @Id (org.springframework.data.annotation.id) 를 함께 사용합니다.
  • @Id로 지정된 필드는 현재 값이 null인 경우 새 id를 생성하거나 이미 설정된 id 값을 재사용하고 키스페이스:id 패턴의 키로 Redis 해시 내에 Domain 객체 유형의 프로퍼티를 저장합니다.

[✨ Redis CRUD Domain Object Example]

  1. @RedisHash(value, timeToLive)
    - Domain Object를 Redis Hash 자료구조로 변환하는 방식, value에 특정한 값을 넣어줌으로써, 데이터가 저장될 때 key의 prefix를 지정합니다. 또한 timeToLive : 입력한 숫자만큼 "초" 단위로 유효기간을 지정합니다.
  2. @Id : Redis의 해시 키로 지정되는 값입니다. 실제 객체가 Redis 에 저장될 때 [키스페이스(위의 value값):id] 로 HSET, HHASH 의 Key 값으로 저장됩니다.
  3. @Indexed : Secondary Indexes 의 역할을 하도록 지정합니다. hset 해당 값도 저장되어 빠른 탐색에 용이하게 합니다.
  4. @TimeToLive : 위의 timeToLive 옵션과 동일한 기능을 수행합니다. 단 유닉스타임스템프로 지정해야합니다.
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@RedisHash(value = "ticket_count", timeToLive = 30)
@Getter
@ToString
public class TicketCount {

    @Id
    @Indexed
    private String id;
    @Indexed
    private String ticketName;
    private int count;
//    @TimeToLive
//    private long expire;

    public TicketCount(String ticketName, int count) {
        this.ticketName = ticketName;
        this.count = count;
    }
}

 

[✨ Redis Repository Example]

public interface RedisCrudRepository extends CrudRepository<TicketCount, String> {

    Optional<TicketCount> findByTicketName(String ticketName);
}

 

[✨ Test]

@SpringBootTest
public class RedisTest {

    @Autowired
    RedisCrudRepository redisCrudRepository;

    @Test
    void test() {
        TicketCount ticketCount = new TicketCount(
            "흠뻑쑈티켓",
            0
        );

        //create
        redisCrudRepository.save(ticketCount);

        //read
        String id = ticketCount.getId();
        System.out.println(id);
        System.out.println(redisCrudRepository.findByTicketName(ticketCount.getTicketName()));
        System.out.println(redisCrudRepository.findById(id));

    }
}

생성된 Redis Hash 구조 살펴보기

 

👏 단, Redis 리포지토리는 Redis Server 버전 2.8.0 이상이 필요하며 트랜잭션과 함께 작동하지 않는다고 합니다.

Sprign Transaction 동작에 Redis를 포함시키고 싶다면 RedisTemplate을 사용해야 합니다.


3. Redis Transaction

  • 기본적으로 RedisTemplate 또한 Spring Transaction 에 포함되지는 않는다고 합니다.
  • Redis 트랜잭션을 사용하려면 설정 시 트랜잭션 지원을 명시적으로 활성화해야 합니다.
@Bean
public StringRedisTemplate redisTemplate() {
    StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
	
    // explicitly enable transaction support
    template.setEnableTransactionSupport(true);              
    return template;
}

 

Spring Redis Transaction 동작과정

  1. 트랜잭션 지원을 활성화하면 현재 스레드 로컬이 지원하는 트랜잭션에 RedisConnection이 바인딩됩니다.
  2. 트랜잭션이 오류 없이 완료되면 Redis 트랜잭션은 EXEC로 커밋되고, 그렇지 않으면 DISCARD로 롤백됩니다.
  3. Redis 트랜잭션은 배치 지향이며 진행 중인 트랜잭션 중에 실행된 명령은 큐에 대기하고 있다가 트랜잭션을 커밋할 때만 적용됩니다.

또한 Spring Data Redis는 진행 중인 트랜잭션에서 읽기 전용 명령과 쓰기 명령을 구분합니다.

  • KEYS와 같은 읽기 전용 명령은 읽기를 허용하기 위해 새로운(스레드에 바인딩되지 않은) RedisConnection으로 파이프됩니다.
  • 쓰기 명령은 RedisTemplate에 의해 큐에 대기하고 커밋 시 적용됩니다.

 


Redis Transaction 직접 구현

  • 혹은 아래와 같이 롤백 파이프라인을 직접 구현할 수 있습니다.
//execute a transaction
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
  public List<Object> execute(RedisOperations operations) throws DataAccessException {
    operations.multi();
    operations.opsForSet().add("key", "value1");

    // This will contain the results of all operations in the transaction
    return operations.exec();
  }
});
System.out.println("Number of items added to set: " + txResults.get(0));

 

 

다음 포스팅에서는 Redis Cache 를 사용해보겠습니다

 

 


* 참고

추가 살펴보면 좋을 내용