Spring
Spring Redis에 Transaction 적용하기
mysterious dev
2024. 2. 3. 21:17
Redis Transaction
레디스에서는 트랜잭션을 지원하기 위해 다음 명령어를 지원한다.
- MULTI: 트랜잭선을 시작한다. 이후 들어오는 명령어는 곧바로 처리되지 않고 queue 쌓인다.
- EXEC: 커밋과 같다. queue에 쌓인 명령어를 일괄적으로 실행한다.
- DISCARD: 롤백과 같다. queue의 명령어를 일관적으로 버린다.
- WATCH: Lock과 동일한 역할을 한다. (낙관적 락 방식)
프로젝트 설정
✔️ dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
✔️ application.yml
spring:
data:
redis:
host: localhost
port: 6379
Spring Redis에 트랜잭션 적용
아무런 설정을 하지 않는다면, 기본적으로는 트랜잭션 기능이 동작하지 않는다.
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
@Getter
public class SampleObject {
private Long id;
private String name;
public SampleObject(Long id, String name) {
this.id = id;
this.name = name;
}
}
@RequiredArgsConstructor
@Transactional
@Service
public class RedisTxService {
private final RedisTemplate<String, SampleObject> redisTemplate;
public void save(Long id, String name) {
System.out.println("processing ...");
redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
if (name == "ex") {
throw new RuntimeException("name is ex!!");
}
}
}
트랜잭션이 적용된다면 예외 발생 시 데이터가 모두 롤백되어야 한다.
@SpringBootTest
class RedisTxServiceTest {
@Autowired
private RedisTxService redisTxService;
@Autowired
private RedisTemplate<String, SampleObject> redisTemplate;
@Test
void 기본적으로_트랜잭션이_적용되지_않는다() {
// when
assertThatThrownBy(() -> {
redisTxService.save(1L, "ex");
}).hasMessage("name is ex!!");
// then
assertThat(redisTemplate.opsForValue().get("sample:"+1L))
.isNotNull();
}
}
위 테스트를 진행하면 성공하는 것을 알 수 있는데, 이를 통해 기본적으로 트랜잭션이 적용되지 않는다는 것을 알 수 있다.
✔️ 트랜잭션 설정
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setEnableTransactionSupport(true);
return redisTemplate;
}
}
위와 같이 트랜잭션 옵션을 활성화시켜준 RedisTemplate을 등록하면 트랜잭션이 적용된다.
Redis 단독으로 사용하게 되면 @EnableTransactionManager를 추가로 달아주어야 하며,
PlatformTransactionMananer를 빈으로 등록해야 한다.
jpa를 사용중이라면 위 과정이 자동으로 진행되므로 RestTemplate만 등록하면 된다.
@SpringBootTest
class RedisTxServiceTest {
@Autowired
private RedisTxService redisTxService;
@Autowired
private RedisTemplate<String, SampleObject> redisTemplate;
@Test
void 기본적으로_트랜잭션이_적용되지_않는다() {
// when
assertThatThrownBy(() -> {
redisTxService.save(1L, "ex");
}).hasMessage("name is ex!!");
// then
assertThat(redisTemplate.opsForValue().get("sample:"+1L))
.isNull();
}
}
예외 발생 시 트랜잭션이 적용된다면 롤백되어야 하므로 isEmpty()로 바꿨다.
결과는 다음과 같이 성공하는 것을 알 수 있다.
✔️ @Transactional(readOnly = true) 사용 시
ReadOnly 트랜잭션에서도 데이터는 저장된다.
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class RedisTxService {
private final RedisTemplate<String, SampleObject> redisTemplate;
public void save(Long id, String name) {
System.out.println("processing ...");
redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
if (name == "ex") {
throw new RuntimeException("name is ex!!");
}
}
}
@SpringBootTest
class RedisTxServiceTest {
@Autowired
private RedisTxService redisTxService;
@Autowired
private RedisTemplate<String, SampleObject> redisTemplate;
@Test
void readOnly_여도_데이터는_저장된다() {
// when
redisTxService.save(1L, "name");
// then
assertThat(redisTemplate.opsForValue().get("sample:" + 1L))
.isNotNull();
}
}
트랜잭션이 readOnly인 경우 MULTI 자체를 사용하지 않는다.
✔️ TransactionTemplate 사용 시
TransactionTemplate을 사용해도 트랜잭션을 적용할 수 있다.
@RequiredArgsConstructor
@Service
public class RedisTxService {
private final RedisTemplate<String, SampleObject> redisTemplate;
private final TransactionTemplate transactionTemplate;
public void save(Long id, String name) {
transactionTemplate.executeWithoutResult((status -> {
System.out.println("processing ...");
redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
if (name == "ex") {
throw new RuntimeException("name is ex!!");
}
}));
}
}
@SpringBootTest
class RedisTxServiceTest {
@Autowired
private RedisTxService redisTxService;
@Autowired
private SampleObjectRepository sampleObjectRepository;
@Test
void transactionTemplate_을_통해서도_tx_적용_가능() {
// when
assertThatThrownBy(() -> {
redisTxService.save(1L, "ex");
}).hasMessage("name is ex!!");
// then
assertThat(redisTemplate.opsForValue().get("sample:"+1L))
.isNull();
}
}
주의점
Spring data redis에서 제공되는 RedisRepository에는 트랜잭션을 적용할 수 없다.
(관련 이슈)