무지를 아는 것이 곧 앎의 시작

Spring

RedisTemplate는 @Transactional 안에서 어떻게 동작할까?

Alex96 2023. 6. 27. 02:12

최근 사이드 프로젝트를 하며 재밌는 사실을 알게 되어 포스팅을 남겨본다.

 

RedisTemplate를 사용할 때 스프링에서 추상화하여 제공해주는 트랜잭션 기능을 사용하고 싶으면 다음과 같은 세팅을 하면 된다.

@Configuration
@RequiredArgsConstructor
@EnableTransactionManagement
public class RedisConfig {

    //...

    @Bean
    public RedisTemplate<String, String> redisTemplate(
            final RedisConnectionFactory redisConnectionFactory
    ) {
        final RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setEnableTransactionSupport(true); // 이거이거
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
    
    //...
}

보통 멀티 데이터 소스의 트랜잭션을 엮어주려면 ChainedTransactionManager를 사용해서 엮어주는데 얘는 좀 특이하게 setEnableTransactionSupport() 메소드에 true값을 세팅해주면 @Transactional이 붙은 메소드 안에서 트랜잭션 지원을 받는다.

 

이렇게 설정을 하고 개발 후 테스트 코드를 짜던 도중..

테스트 코드
결과

분명 저장하고 조회했는데 결과가 0이란다..😱

디버깅 찍어봤는데 진짜 저 조회에서 응답 받은게 없다.

 

처음엔 내가 세팅을 잘못해서 버그가 생긴 줄 알았다.

근데 이상하게 테스트 코드가 아니라 실제 실행하여 테스트할 땐 원하는 동작을 잘 하고 있었다.

 

의심가는 놈은 이 친구...

트랜잭셔널..?

@Transactional 어노테이션은 프로덕션 코드에선 메소드의 로직을 하나의 트랜잭션으로 묶어 메소드가 종료 후에 커밋을 호출하지만, 테스트 코드에선 마지막에 롤백을 호출한다. 그렇다면 Redis의 트랜잭션은 어떻게 동작하길래..?

 

MULTI/EXEC

Redis의 트랜잭션은 그냥 명령어를 모아서 한꺼번에 실행시키는게 끝이다.

MULTI라는 명령어를 입력하면 이후 입력하는 명령어들이 명령어 큐에 담긴다.

그런 다음 EXEC라는 명령어를 입력하면 명령어들이 큐에서 순서대로 실행되는 방식이다.

그래서 조회가 안됐던 거다.... 테스트 코드의 메소드의 트랜잭션은 메소드가 종료될 때 커밋되니 레디스의 트랜잭션을 종료하는 EXEC도 아직 호출되지 않았던 거다..

 

새삼 RDBMS의 트랜잭션이 이것 저것 지원해주는게 참 많다고 느낀다.. 

 

그냥 따로 격리 구현..

그래서 그냥 테스트 클래스에서 @Transactional을 빼고 손수 테스트 격리를 구현했다.

@SpringBootTest
class RefreshTokenRedisListRepositoryTest {

    // ....

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @AfterEach
    void tearDown() {
        final Set<String> keys = redisTemplate.keys("member::refreshToken::*");
        if (keys != null) {
            redisTemplate.delete(keys);
        }
    }
    
    // ....
}

메소드 실행 이후에 키 패턴으로 조회해서 전부 날려줬다. 테스트 환경 레디스는 임베디드 레디스를 쓰고 있어서 전체 keys를 조회해도 딱히 별 문제가 없었다. 그리고 레디스를 테스트해야하는 곳에서만 이렇게 하고 다른 협력테스트에선 MockRepository를 따로 구현해서 사용하게 설정했다. 신경쓸게 많은 레디스..👊🏿

 

 

이번 문제는 다행히 테스트 코드에서 터졌는데 만약 프로덕션 코드에서 레디스에 데이터를 삽입한 후 삽입한걸 조회해야 하는 로직이 있다면 주의해야할 것 같다. 최대한 Redis에 저장하는 데이터는 그 트랜잭션에선 다시 안읽는 방향이 좋을 것 같고...

만약 부득이하게 읽어야 한다면 트랜잭션을 분리해줘야 하는데 관리 포인트가 늘어나며, 원자성도 깨질 수 있다는걸 명심하고 보정 정책을 마련해야할 듯 하다.

 

 

ps) 이거 하이버네이트 영속성 컨텍스트처럼 내가 삽입한 데이터 따로 스레드 로컬에 보관하다가 읽을때 내주는 모듈을 구현해보면 어떨까..?🤔 시간날때 토이로 해봐야겠다 ㅋㅋㅋ

'Spring' 카테고리의 다른 글

멀티 모듈 프로젝트 세팅하기  (0) 2023.11.27
내편 로깅 정책 정리  (0) 2022.10.24
로깅 성능에 대한 고민  (0) 2022.10.24
로그 잘 남기면 디버깅이 수월해짐  (0) 2022.10.23
요청로깅 AOP로 해야겠다  (0) 2022.10.23