프로젝트의 RefreshToken과 1:1채팅의 읽음 처리를 구현하기 위해 사용자가 소켓에 접속했는지 저장하기 위해 Redis를 사용하기로 했다.
사용자가 소켓에 접속했는지 관리하려면 소켓에 접속하고 나갈 때 마다 계속 데이터를 저장하고 삭제해줘야 하는데 RDBMS 보다 Redis에 저장하는 것이 더 효율적이라고 판단했으며 이전부터 Redis 라는 기술을 사용해보고 싶었다.
기존엔 구현하기 쉽다는 이유로 CrudRepository를 이용해 간단하게 구현했다. 하지만 CrudRepository를 사용하니 실제 Redis에 저장된 값들이 직관적이지 못 했으며 Transaction을 지원하지 않았다.
@Getter
@RedisHash(value = "refreshToken", timeToLive = 604800)
public class RefreshToken {
@Id
private String id;
private String ip;
private Set<String> authorities;
@Indexed
private String refreshToken;
@Indexed
private String username;
@Builder
public RefreshToken(String ip, Set<String> authorities, String refreshToken, String username) {
this.ip = ip;
this.authorities = authorities;
this.refreshToken = refreshToken;
this.username = username;
}
}
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
RefreshToken findByRefreshToken(String refreshToken);
RefreshToken findByUsername(String username);
}
RefreshToken 클래스에 @RedisHash 어노테이션을 붙이고 CrudRepository를 이용해 쉽게 Redis에 저장할 수 있었다. 하지만
@Test
void saveRepositoryTest() {
// given
Set<String> roles = new HashSet<>();
roles.add("ROLE_USER");
RefreshToken refreshToken = RefreshToken.builder()
.ip("mockIp")
.refreshToken("mockRefreshToken")
.username("mockUsername")
.authorities(roles)
.build();
// when
RefreshToken savedRefreshToken = refreshTokenRepository.save(refreshToken);
RefreshToken findRefreshToken = refreshTokenRepository.findById(savedRefreshToken.getId()).orElseThrow(
() -> new IllegalArgumentException()
);
// then
assertThat(findRefreshToken.getId()).isEqualTo(savedRefreshToken.getId());
assertThat(findRefreshToken.getRefreshToken()).isEqualTo(savedRefreshToken.getRefreshToken());
assertThat(findRefreshToken.getUsername()).isEqualTo(savedRefreshToken.getUsername());
assertThat(findRefreshToken.getAuthorities()).isEqualTo(savedRefreshToken.getAuthorities());
assertThat(findRefreshToken.getIp()).isEqualTo(savedRefreshToken.getIp());
}
해당 테스트를 실행한 뒤 실제 Redis에 저장된 값을 보니


위와 같이 저장되었으며 값들의 타입이 모두 달라 Redis에서 내가 입력한 값이 잘 저장되었는지 확인하기 어려웠다. 이러한 이유들로 인해 난 CrudRepository가 아닌 RedisTemplate을 사용하기로 결정했다.
아래는 RedisTemplate을 테스트해보며 알아낸 사실들이다.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
// redisTemplate를 받아와서 set, get, delete를 사용
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
// setKeySerializer, setValueSerializer 설정
// redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setEnableTransactionSupport(true);
return redisTemplate;
}
}
위 코드를 이용해 RedisTemplate을 빈으로 등록하며 redisTemplate.setEnableTransactionSupport를 true로 설정해 transaction이 동작하도록했다.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.expire(username, 7L, TimeUnit.DAYS);
if (username.equals("username")) {
throw new RuntimeException();
}
}
말도안되는 테스트코드 이지만 redisTemplate.opsForHash().putAll()을 통해 username 값을 키로 토큰값을 Redis의 해쉬 자료구조를 이용해 저장하고 강제로 Exception을 발생시킨다.

그러나 예외를 발생시켜 테스트가 실패했음에도 Redis에는 값이 저장된다.
@Test
@Transactional
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.expire(username, 7L, TimeUnit.DAYS);
if (username.equals("username")) {
throw new RuntimeException();
}
}
그러나 @Transactional 어노테이션을 테스트 메서드에 붙여주면

테스트는 똑같이 실패하지만 Redis에 값이 들어가지 않았다. Transaction이 잘 적용됐음을 알 수 있다.
아래는 RedisTemplate을 이용해 여러가지 테스트를 실행했을때의 결과들이다.
1.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.opsForHash().put(username, "additional", "value");
}

opsForHash를 이용해 username을 키로 Map의 값을 넣은 뒤 같은 키로 서브키를 추가시키면 서브키의 값이 정상적으로 추가됨을 볼 수 있다.
2.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.opsForHash().put(username, "role", "value");
}

opsForHash를 이용해 username을 키로 Map의 값을 넣은 뒤 같은 uername 키에 이미 존재하는 서브키의 값을 넣으면 기존의 값이 덮어써짐을 볼 수 있다.
3.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.opsForValue().set(username, "value");
}

opsForHash를 이용해 username을 키로 Map의 값을 넣은 뒤 opsForValue를 이용해 같은 키로 값을 넣으면 해쉬 형식으로 저장됐던 값들이 뒤에 저장한 String 타입으로 덮어써졌음을 볼 수 있다.
4.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForValue().set(username, "value");
redisTemplate.opsForHash().putAll(username, refreshToken);
}

3. 과 반대로 opsForValue로 먼저 값을 넣은 뒤 opsForHash로 값을 넣으면 덮어써지는 것이 아닌 오류를 뱉는다. Redis에는 먼저 값을 넣은 String 형식의 값이 저장되어 있다.
5.
@Test
void redisTest() {
// given
String token = "refreshToken";
String username = "username";
String role = "role";
String ip = "ip";
Map<String, String> refreshToken = RefreshTokenMapper.refreshTokenMap(token, username, role, ip);
redisTemplate.opsForHash().putAll(username, refreshToken);
redisTemplate.expire(username, 7L, TimeUnit.DAYS);
}

마지막으로 redisTemplate.expire을 이용하면 Redis에 저장기간을 설정할 수 있다. (expire을 설정하지 않으면 Redis에 ttl이 -1로 저장되어 영구 저장된다.)
'Project > DB' 카테고리의 다른 글
| [Independe] 채팅 DB 설계에 대한 고민 (2) | 2023.12.05 |
|---|---|
| [Independe] 추천, 신고, 즐겨찾기 DB 설계에 대한 고민 (0) | 2023.09.15 |
| [Independe] 대댓글 설계에 대한 고민 (1) | 2023.09.14 |