이전 글에서 Redis를 Master + Slave로 구성했다. 오늘은 그 구성을 Spring에 적용해보고 정말 그림대로 작도앟는지 확인해보자!
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.7.0'
# redis 설정
redis:
master:
host: 127.0.0.1
port: 6379
slaves:
- host: 127.0.0.1
port: 7000
- host: 127.0.0.1
port: 7001
yml에 redis의 설정을 추가해주자.
@Getter
@Setter
@NoArgsConstructor
@ConfigurationProperties(prefix = "redis") // 설정 값을 불러올 때 prefix 값을 지정할 수 있다.
@Configuration
public class RedisInfo {
private String host;
private int port;
private RedisInfo master;
private List<RedisInfo> slaves;
}
Config의 구성은 다음과 같이 자기 자신을 불러 담고 있고 각 master와 slaves로 구성된다.
약간 노드 트리 구조 같기도?
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
private final RedisInfo info;
@Bean
public LettuceConnectionFactory redisConnectionFactory(){
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // replica에서 우선적으로 읽지만 replica에서 읽어오지 못할 경우 Master에서 읽어옴
.build();
// replica 설정
RedisStaticMasterReplicaConfiguration slaveConfig = new RedisStaticMasterReplicaConfiguration(info.getMaster().getHost(), info.getMaster().getPort());
// 설정에 slave 설정 값 추가
info.getSlaves().forEach(slave -> slaveConfig.addNode(slave.getHost(), slave.getPort()));
return new LettuceConnectionFactory(slaveConfig, clientConfig);
}
}
이렇게 작성하면 된다. 막상 해보니 굳이 RedisInfo를 만들지 않고 env 값으로 그대로 가져왔어도 될거 같긴하다. 그래도 설정 값을 그대로 사용하는 것보단 최대한 객체로 만들어서 사용하는게 더 좋은 코드 같으니 이대로 진행하자!
@RestController
@RequestMapping("/redis")
@RequiredArgsConstructor
public class RedisController {
private final RedisService redisService;
@PostMapping("/save")
public ResponseEntity<CommonResponse<String>> saveUser(@RequestBody JoinUserDto user){
redisService.saveUser(user);
return ResponseEntity.ok(new CommonResponse<String>(0, "성공"));
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class JoinUserDto {
private String id;
private String name;
}
dto와 함께 생성했다.
public interface RedisService {
void saveUser(JoinUserDto user);
}
@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
private final StringRedisTemplate template;
final ObjectMapper objectMapper;
public RedisServiceImpl(StringRedisTemplate template, ObjectMapper objectMapper) {
this.template = template;
objectMapper.registerModule(new JavaTimeModule()); //object mapper에서 java localDateTime 사용시 에러 때문에 추가
this.objectMapper = objectMapper;
}
@Override
public void saveUser(JoinUserDto user) {
String value = "";
try {
value = objectMapper.writeValueAsString(user);
} catch (JsonProcessingException e) {
log.error("유저를 redis에 저장하던 도중 에러가 발생했습니다.");
throw new RuntimeException(e.getMessage());
}
template.opsForHash().put("user", user.getId(), value);
}
}
이제 테스트를 통해 결과를 확인해보자.
다음과 같이 요청했고
모두 동일하게 데이터가 들어간 것을 확인할 수 있다.
그럼 과연 우리가 구성한대로 read는 slave에서 해오는게 맞을까??
get을 만들어서 테스트 해보자
@RestController
@RequestMapping("/redis")
@RequiredArgsConstructor
public class RedisController {
private final RedisService redisService;
...
@GetMapping("/get/{id}")
public ResponseEntity<CommonResponse<ResponseUserDto>> getUser(@PathVariable String id){
ResponseUserDto response = redisService.getUser(id);
return ResponseEntity.ok(new CommonResponse<ResponseUserDto>(0, response));
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ResponseUserDto {
private String id;
private String name;
}
public interface RedisService {
void saveUser(JoinUserDto user);
ResponseUserDto getUser(String id);
}
@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
private final StringRedisTemplate template;
final ObjectMapper objectMapper;
...
@Override
public ResponseUserDto getUser(String id) {
String value = template.opsForHash().get("user", id).toString();
ResponseUserDto response = null;
try {
response = objectMapper.readValue(value, ResponseUserDto.class);
} catch (JsonProcessingException e) {
log.error("redis에서 데이터를 불러오던 도중 에러가 발생했습니다.");
throw new RuntimeException(e.getMessage());
}
return response;
}
}
코드를 실행시키면
다음 결과를 받아올 수 있다.
이제 정말 master가 아닌 slave인지 확인해보자
테스트를 진행하기 전 위의 결과를 확인했다면 이제 master가 되는 redis 서버를 정지 시킨다.
나의 경우 다음과 같이 docker를 사용하여 정지시켰다.
명령어로 정지 시킬 경우 docker stop [container 이름]으로 정지시키면 된다.
그 후에 다음과 같이 동일하게 요청하면 master 서버가 죽어도 정상적으로 반환 값이 오는 것을 확인할 수 있다!
그럼 반대로 set을 하는 부분을 실행해보자.
1분이 경과하니 timeout 에러가 나며 종료되었다.
그냥 궁금해서 만약 master만 살아있고 slave는 다 죽었을 때 select는 어떻게 해올까?
다시 정상적으로 save가 되는 것을 확인하고
slave를 모두 종료시켰다.
get을 했는데 그냥 msater에서 가져오는 것 같다.
그리고 해당 내용은 우리가 이미
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
private final RedisInfo info;
@Bean
public LettuceConnectionFactory redisConnectionFactory(){
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // replica에서 우선적으로 읽지만 replica에서 읽어오지 못할 경우 Master에서 읽어옴
.build();
// replica 설정
RedisStaticMasterReplicaConfiguration slaveConfig = new RedisStaticMasterReplicaConfiguration(info.getMaster().getHost(), info.getMaster().getPort());
// 설정에 slave 설정 값 추가
info.getSlaves().forEach(slave -> slaveConfig.addNode(slave.getHost(), slave.getPort()));
return new LettuceConnectionFactory(slaveConfig, clientConfig);
}
}
readFrom
에서 설정했었다!