본문 바로가기
프로그래밍/redis

[redis + Spring] Spring Data Redis를 이용한 Transaction 처리

by 사바라다 2021. 7. 22.

안녕하세요. 오늘은 Java의 Spring 환경에서 Transaction을 적용해보도록 하겠습니다.

의존성

먼저 Spring에서 Redis를 사용하기 위해서 아래와 같은 의존성을 부여하도록 하겠습니다. spring-boot-starter의 버전은 본인이 사용하시는 SpringBoot 버전을 기본적으로 따라가기 때문에 버전은 따로 명시하지 않았습니다.

implements("org.springframework.boot:spring-boot-starter-data-redis")

환경설정

아래는 redis와 연결하기 위한 관련된 application.yml 파일입니다.

spring:
  redis:
    port: 6379
    host: localhost

 

Redis도 @Transactional을 이용해서 트랜잭션을 관리할 수 있습니다. 이렇게 해주기 위해서는 PlatformTransactionManager를 Bean으로 등록해야합니다. 하지만 Spring Data Redis는 PlatformTransactionManager를 제공해주고 있지 않습니다. 따라서 JDBC 또는 JPA 등 여타 transaction managers를 가지고 있는 의존성에 기생(?)해서 사용할 수 밖에 없습니다.

이런 부분 때문에 환경 설정부분은 2가지로 나누어집니다. 다른 DBMS를 사용하고 있는경우와 그렇지 않은 경우입니다. 먼저 타 DBMS의 의존성을 사용하고 계시다면 AutoConfiguration에 의해서 @EnableTransactionManagement가 사용되어지고 있을겁니다. 따라서 아래처럼 환경설정을 잡아주시기만 하면 @Transacstional을 통해서 Redis의 Transaction을 손쉽게 사용하실 수 있습니다.

@Configuration
public class RedisConfig {

  @Bean
  public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setEnableTransactionSupport(true); // redis Transaction On !
    return redisTemplate;
  }

  @Bean // 만약 PlatformTransactionManager 등록이 안되어 있다면 해야함, 되어있다면 할 필요 없음
  public PlatformTransactionManager transactionManager() throws SQLException {
      // 사용하고 있는 datasource 관련 내용, 아래는 JDBC
    return new DataSourceTransactionManager(datasource()); 

    // JPA 사용하고 있다면 아래처럼 사용하고 있음
    return new JpaTransactionManager(entityManagerFactory);
  }
}

추가로 redis 단독으로 사용하고 있으시다면 @EnableTransactionManagement를 명시적으로 On 시켜주어야합니다. 그러기 위해서는 아래의 의존성이 추가로 필요해집니다.

implementation("org.springframework:spring-tx:{version}") // 최신 버전 : '5.3.9'

Redis의 환경설정 코드는 아래와 같아집니다.

@Configuration
@EnableTransactionManagement                                 
public class RedisTxContextConfiguration {

  @Bean
  public StringRedisTemplate redisTemplate() {
    StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
    // explicitly enable transaction support
    template.setEnableTransactionSupport(true);              
    return template;
  }

  @Bean
  public PlatformTransactionManager transactionManager() throws SQLException {
    return new DataSourceTransactionManager();   
  }
}

Transaction 예제

위 처럼 설정을 했으면 이제 실제로 Transaction을 동작시켜보도록 하겠습니다.

redisTemplate

이전 [redis] 트랜잭션(Transaction) 포스팅에서 Redis의 transaction이 어떻게 이루어지는지에 대해서 알아보았습니다. transaction을 유지하기 위해서는 동일한 connection을 유지할 필요가 있습니다. 그런데 일반적인 redisTemplate 명령어는 connection을 유지하지 않습니다. connection을 유지하기 위한 명령어로 SessionCallback를 사용할 필요가 있습니다.

아래 코드를 보면서 이해해보도록 합시다.

List<Object> txResults = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        operations.multi(); // redis transaction 시작

        operations.opsForValue().set("SABARADA", "1");
        operations.opsForValue().set("KAROL", "2");

        return operations.exec(); // redis transaction 종료
    }
});

System.out.println("return values of items : " + Arrays.toString(txResults.toArray()));

위 코드를 실행했을 때 아래와 같은 결과를 얻게되며 redis에는 정상적으로 데이터가 쓰여진 사실을 알 수 있었습니다.

return values of items : [true, true]

아래 코드는 일부러 실패하도록 코드를 만들어보았습니다. if 절의 sideEffect에 의해서 아래 코드는 Exception을 발생시킵니다. 이후 데이터가 redis에 쓰여지지 않은것을 확인할 수 있었습니다.

boolean sideEffect = true;

List<Object> txResults = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
    public List<Object> execute(RedisOperations operations) throws DataAccessException {
        operations.multi();

        operations.opsForValue().set("SABARADA", "1");
        operations.opsForValue().set("KAROL", "2");

        if (sideEffect) {
            throw new RuntimeException("exception occur");
        }

        return operations.exec();
    }
});

@Transactional

@Transactional을 사용해서 redis Transaction을 유지하기 위해서는 redisTemplate 설정에 setEnableTransactionSupport(true)를 추가해야합니다. @Transactional를 redis와 이용하면 기본적으로 ThreadLocal 기반으로 메서드 시작시 transaction 시작으로 MULTI, 메서드 종료시 transaction 커밋으로 EXEC 명령어를 실행하는 것으로 구현하고 있습니다. 만약 Exception이 발생하면 DISCARD가 실행됩니다.

추가적으로 @Transactional를 사용했을 경우 Read-only 커맨드는 Transaction Queue에 들어가지 않고 바로 실행되며 Write 커맨드만 들어가게 되는점을 알아두시기 바랍니다.

사용은 아래 코드처럼 할 수 있습니다.

@Transactional
public void transactionService2() {

    boolean sideEffect = false;

    stringRedisTemplate.opsForValue().set("SABARADA", "1");
    stringRedisTemplate.opsForValue().set("KAROL", "2");

    if (sideEffect) {
        throw new RuntimeException("exception occur");
    }

    String sabarada = stringRedisTemplate.opsForValue().get("SABARADA");

    System.out.println("결과 =  " + sabarada); // 결과 = 1
}

마무리

오늘은 이렇게 Spring Data Redis를 이용해서 트랜잭션 예제를 처리해보았습니다.

추가적으로 말씀 드리면 @Transactional은 ThreadLocal 기반이기 때문에 reactive 환경에서는 에서는 동작하지 않습니다. reactive 환경에서 Transactional을 유지하기 위해서는 Netty 기반의 Redisson을 이용해야합니다.

감사합니다.

참조

spring_redis_docs

댓글