Redis + Spring Boot (Master + Slave) 적용하기

최준호·2022년 8월 17일
2

Spring + Redis

목록 보기
2/4
post-thumbnail

이전 글에서 Redis를 Master + Slave로 구성했다. 오늘은 그 구성을 Spring에 적용해보고 정말 그림대로 작도앟는지 확인해보자!

📕 Spring Boot 프로젝트에 적용

📄 gradle 추가

// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.7.0'

📄 yml 설정

# 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의 설정을 추가해주자.

📄 Info 파일 작성

@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로 구성된다.

약간 노드 트리 구조 같기도?

📄 Config 파일 작성

@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 값으로 그대로 가져왔어도 될거 같긴하다. 그래도 설정 값을 그대로 사용하는 것보단 최대한 객체로 만들어서 사용하는게 더 좋은 코드 같으니 이대로 진행하자!

📄 Controller 작성

@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와 함께 생성했다.

📄 service 작성

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을 만들어서 테스트 해보자

📄 Controller 작성

@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;
}

📄 Service 작성

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에서 설정했었다!

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글