티스토리 뷰

Spring-boot

[Spring boot]Redis - Lettuce 설정

개발자-김씨 2021. 5. 6. 10:47
반응형

spring boot 학습 & 세팅 5탄 (2.3.9 RELEASE  docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/)

spring boot 레디스 설정 1편

 

 

1. 사전 작업

테스트를 위해 로컬에 redis서버를 설치하고 포트는 기본 포트(6379)로 띄웠습니다. (혹은 내장 redis서버를 이용해서 로컬환경을 구성할 수도 있습니다. )

 

2. 의존성 추가 + redis서버 설정(application.yml)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
spring:
    redis:
        host: 127.0.0.1
        port: 6379
        timeout: 3000 #ms connection timeout

autoConfiguration에 의해 restTemplate과 redisConnectionFactory가 자동 생성되기 때문에 이렇게만 해도 redis연동이 끝난다.

 

3. RedisTemplate

spring-boot parent 2.2이상 버전에서는 자동 설정에 의해서 redis template 빈이 4개가 생성됩니다.

    @Autowired RedisTemplate redisTemplate;

    @Autowired StringRedisTemplate stringRedisTemplate;

    @Autowired ReactiveRedisTemplate reactiveRedisTemplate;

    @Autowired ReactiveStringRedisTemplate reactiveStringRedisTemplate;

@EnableAutoConfiguration(exclude={RedisReactiveAutoConfiguration.class})로 reactive redis template은 자동 설정을 disable시킬 수 있습니다.

redisTemplate와 stringRedisTemplate의 차이는 Serializer(직렬화)입니다.

redisTemplate의 key, value serializer는 JdkSerializationRedisSerializer이고

stringRedisTemplate의 serializer StringRedisSerializer입니다.

직렬화 방식이 다르기 때문에 혼용해서 사용하는 건 안 됩니다.

@Test
public void test() {

    redisTemplate.opsForValue().set("key", "hello");

    log.debug("redisTemplate get value => {}", redisTemplate.opsForValue().get("key"));
    //==> "hello"
    
    log.debug("stringRedisTemplate get value => {}", stringRedisTemplate.opsForValue().get("key"));
    //==> null

 

만약 다른 직렬화 방식을 사용한다면 RedisTemplate bean을 생성해야 합니다. (RedisConnectionFactory은 자동 설정에 의해 생성된 빈을 주입받아 사용)

    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.java());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(RedisSerializer.java());
        return redisTemplate;
    }

bean 네임을 "redisTemplate"로 하면 자동 설정(RedisAutoConfiguration)이 redisTempate은 생성하지 않는다.

public class RedisAutoConfiguration {
 
    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        ....

 

 

4. Lettuce

lettuce(레터스)는 redis 클라이언트 api로 기본적인 내용은 인터넷에 많으니 pass ~

지금까지 정리한 내용 중에 레터스와 관련된 내용은 없지만 spring boot 2부터는 레터스가 기본이기 때문에 자동 설정에 의해 Redis Connection factory로 레터스가 사용된다. 스프링부트에서는 일반적으로 RedisTemplate추상화 레벨을 이용하므로 lettuce api를 직접 호출할 일은 없기 때문에 커넥션 & 커넥션 풀에 대한 내용만 알면 된다.

Lettuce, on the other hand, is built on netty and connection instances can be shared across multiple threads. So a multi-threaded application can use a single connection regardless of the number of concurrent threads that interact with Lettuce.

레터스는 레디스 서버와 단일 커넥션으로 멀티 스레드 요청에 대해 처리가 가능하다. 물론 내부적으로 논-블럭킹 + 비동기로 구현되어 있으며 스레드 세이프하다. 레디스 서버가 어차피 싱글 스레드 기반이기 때문에 어차피 다중 커넥션이 단일 커넥션에 비해 성능상 이점이 있는 것도 아니다. 따라서 레터스를 사용한다면 굳이 커넥션 풀을 만들지 않고 단일 커넥션을 공유하도록 하는 것이 좋다.

그런데 커넥션을 공유하면 안되는 경우가 있다. 블로킹(차단) API사용이나 레디스 트랜잭션(multi/exec)의 경우이다. 여기까지는 레터스의 내용이다. (레터스 사이트 공식 문서)

 

블로킹의 경우는 spring에서는 어차피 레터스API를 직접 사용할 일이 거의 없기 때문에 무시하면 되고 레디스 트랜잭션의 경우는 spring에서도 사용할 일이 있다. 레디스 트랜잭션을 사용할 경우에는 새 커넥션이나 커넥션 풀을 이용해 전용 커넥션(연결)을 사용하도록 해야 한다. 그럼 스프링부트에서 레터스에 대해 알아보자.

 

일단 shareNativeConnection옵션부터 보자

    public void setShareNativeConnection(boolean shareNativeConnection) {
        this.shareNativeConnection = shareNativeConnection;
    }

org.springframework.data.redis의 LettuceConnectionFactory에 보면 shareNativeConnection옵션이 있는데, 커넥션 공유 여부에 대한 설정이며 기본값은 true이다.

로그 레벨을 디버그로하고 shareNativeConnection=true하고 redis SET, GET명령어를 날려보면

redisTemplate.opsForValue().set("key1", "hello");
log.debug("redisTemplate get value => {}", redisTemplate.opsForValue().get("key1"));
dispatching command AsyncCommand [type=SET, outp
[channel=0xdb7ad8fe, /127.0.0.1:1464 -> /127.0.0.1:6378, epid=0x1] write() writeAndF
....
....
dispatching command AsyncCommand [type=GET, output=ValueO
[channel=0xdb7ad8fe, /127.0.0.1:1464 -> /127.0.0.1:6378, epid=0x1] write() writeAndFl

이런 네티 로그를 볼 수 있는데, SET, GET명령어가 동일한 커넥션(channel=0xdb7ad8fe가 같다.)으로 처리된걸 확인 할 수 있다.

lettuceConnectionFactory.setShareNativeConnection(false)하고 다시 호출해보면 

[channel=0x2d1bff4a, [id: 0x3e56a857] (inactive), chid=0x2] channelRegistered()
Connecting to Redis at 127.0.0.1:6378: Success...
...
dispatching command AsyncCommand [type=SET, outp....
[channel=0x2d1bff4a, /127.0.0.1:1535 -> /127.0.0.1:6378, epid=0x2] write() ...
...
Closing Redis Connection...
[channel=0x2d1bff4a, /127.0.0.1:1535 -> /127.0.0.1:6378, epid=0x2] closeAsync()
...
....
....
dispatching command AsyncCommand [type=GET, output=ValueO...
[channel=0x72087637, /127.0.0.1:1536 -> /127.0.0.1:6378, epid=0x3] write() ....

SET, GET명령어가 다른 커넥션으로 호출된 걸 확인할 수 있다. (channel id가 다르다.) 그리고 명령어 호출 전후로 커넥션을 얻고, 닫는 로그도 확인 할수 있다. 이렇게 shareNativeConnection=false로 주면 커넥션을 공유하지 않는 걸 확인 할 수 있다.

(shareNativeConnection어떻게 동작하는지 코드를 첨부하려고 했는데 내용이 너무 길어질거 같아 다시 지웠습니다. 궁금하시면 LettuceConnectionFactory클래스와 LettuceConnection클래스를 보시면 됩니다.)

따라서 레디스 트랜잭션 실행 시, shareNativeConnection=false로 커넥션을 공유하지 않도록 할 수 있다. 그런데 lettuceConnectionFactory의 옵션값을 변경하면 비-트랜잭션 명령어도 커넥션을 공유하지 않게 되어 커넥션을 공유하는 레터스의 장점을 얻을 수 없게 된다. 만약 비-트랜잭션 명령어는 커넥션을 공유하게 하려면 lettuceConnectionFactory를 2개를 만들어야 한다. 

그럼 어떻게 깔끔하게 스프링에서 레디스 트랜잭션 명령어를 전용 커넥션에서 실행하게 할 수 있을까?

사실 트랜잭션 명령의 경우, 스프링부트에서 알아서 전용 커넥션을 획득한다.

    RedisClusterAsyncCommands<byte[], byte[]> getAsyncConnection() {
        if (this.isQueueing()) {  
            return this.getAsyncDedicatedConnection();
        } else {
            return
            (this.asyncSharedConn != null && this.asyncSharedConn instanceof StatefulRedisConnection ? 
             ((StatefulRedisConnection)this.asyncSharedConn).async() : this.getAsyncDedicatedConnection());
        }
    }
    
    public boolean isQueueing() {
        return this.isMulti;
    }
    
    public void multi() {
	...
	this.isMulti = true;
	...
    }

코드를 보면 커넥션 획득(getAsyncConnection) 시, isMulti값이 true인 경우 전용 커넥션을 획득하도록 되어 있다. (isMulti값은 레디스 multi명령어를 호출한 경우 true이다.) 레디스 multi명령어를 호출하면 shareNativeConnection옵션과 무관하게 무조건 전용 커넥션을 획득하도록 해놨다. 

그럼 shareNativeConnection옵션은 언제 사용할까?

레디스 database를 사용하는 경우에는 커넥션을 공유할 수 없습니다. 따라서 shareNativeConnection를 false로 줘야 합니다. 만약 레디스 database명령어를 (select) 공유 커넥션에서 사용하면 UnsupportedOperationException가 발생합니다.

public class LettuceConnection extends AbstractRedisConnection { 
 ...
 public void select(int dbIndex) {
        if (this.asyncSharedConn != null) {
            throw new UnsupportedOperationException("Selecting a new database not supported due to shared connection. Use separate ConnectionFactorys to work with multiple databases");
        } else {
            

이렇게 특별한 이유가 있는 경우가 아니라면 굳이 shareNativeConnection옵션을 건들지 않는 것이 좋습니다.

 

 

5. 레터스 커넥션 풀

전용 커넥션을 획득하기 위해 매번 커넥션을 획득.반환하게 되면 비용이 많이 발생하기 때문에 커넥션 풀을 사용하여 비용을 절감할 수 있다.

스프링부트에서 레터스 커넥션 풀을 위한 옵션을 제공한다.

spring.redis.lettuce.pool.max-active
spring.redis.lettuce.pool.max-idle
spring.redis.lettuce.pool.max-wait
spring.redis.lettuce.pool.min-idle
spring.redis.lettuce.pool.time-between-eviction-runs

lettuce.pool 옵션이 있으면 커넥션 풀이 생성되며, 전용 커넥션이 필요할 때 커넥션 풀이 사용된다.

레터스 커넥션 풀을 사용하려면 apache common-pool2이 필요하므로 pom.xml에 dependency를 추가해야 합니다.

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.3</version>
</dependency>

(LettuceConnectionFactory @bean이 있으면 자동 설정에 의해 LettuceConnectionFactory이 생성되지 않으니 위 옵션들은 당연히 먹히지 않습니다. 혹시 모르니....)

의존성을 추가하고 아래와 같이 application에 레터스 커넥션 풀 설정을 주고 커넥션 풀이 잘 동작하는지 확인해 보겠습니다.

spring:
  redis:
    host: 127.0.0.1
    port: 6378
    lettuce:
      pool:
        max-active: 8

!! 트랜잭션 명령어를 수행하기 위해서 커넥션 풀을 이용하는 것이기 때문에 pool의 max가 클 필요는 없다. 트랜잭션 명령어 수행 빈도를 따져서 적당한 풀 크기를 설정하자.!! (물론 shareNativeConnection을 false로 주면 모든 커넥션은 커넥션 풀에서 획득합니다.) 

 

lettuceConnectionFactory.setShareNativeConnection(false); // lettuceConnectionFactory autowired

redisTemplate.opsForValue().set("key1", "hello");
log.debug("redisTemplate get value => {}", redisTemplate.opsForValue().get("key1"));

위에서 shareNativeConnection를 false로 주고 테스트했을 때는 커넥션 풀이 없었기 때문에 SET, GET명령이 매번 새로운 커넥션을 생성하고 실행되었는데, 커넥션 풀 옵션을 주고 실행하면 매번 커넥션을 생성하지 않고 커넥션 풀에서 커넥션을 획득하기 때문에 동일한 커넥션에서 명령어가 실행된 걸 확인 할 수 있다.

dispatching command AsyncCommand [type=SET, output=Statu
[channel=0xfb0dd9e3, /127.0.0.1:3838 -> /127.0.0.1:6378, 
....
ispatching command AsyncCommand [type=GET, output=Value
[channel=0xfb0dd9e3, /127.0.0.1:3838 -> /127.0.0.1:6378

 

 

정리

1. 특별한 이유가 없다면 shareNativeConnection옵션은 건들지 말자.

2. 트랜잭션 명령어를 사용하지 않는다면 커넥션 풀을 쓸데없이 만들지 말자.

 

레디스 트랜잭션을 사용하지 않는 경우라면

1. 의존성 추가

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2. application.yml 레디스 서버 설정

spring:
    redis:
        host: 레디스 서버 host
        port: 레디스 서버 port

3. 기본 redisTemplate의 직렬화 방식을 바꾸고 싶다면 redisTemplate 빈 생성

    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.java());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setHashValueSerializer(RedisSerializer.java());
        return redisTemplate;
    }

 

레디스 트랜잭션 명령어를 사용한다면 매번 새 연결 대신 커넥션 풀을 사용하게 하자.

4. common-pool2 의존성 추가

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.3</version>
</dependency>

5. 커넥션 풀 옵션 주기

spring:
  redis:
    host: 127.0.0.1
    port: 6378
    lettuce:
      pool:
        max-active: #기본값 8 
        max-idle: #기본값 8
        max-wait: #기본값 -1ms, 풀에서 커넥션 얻을때까지 대기 시간, 음수면 무기한
        min-idle: #기본값 0, time-between-eviction-runs있을때만 유효
        time-between-eviction-runs: #유휴 커넥션을 제거하는 스레드의 실행 간격

 

 

 

 

참고 :

lettuce.io/core/release/reference/index.html  

docs.spring.io/spring-boot/docs/2.3.9.RELEASE/reference/html/appendix-application-properties.htm

 

반응형

'Spring-boot' 카테고리의 다른 글

[Spring boot] 초기화 코드  (0) 2021.04.28
[Spring boot] Filter 설정  (2) 2021.04.28
[Spring boot]yml configuration  (0) 2021.04.27
[Spring boot] undertow  (0) 2021.04.23
댓글