예약 시스템 - 중복 예약 처리 4 Redis를 이용한 문제 해결

Chan Young Jeong·2024년 3월 17일
0

프로젝트 Dplanner

목록 보기
4/5

앞에서 메시지 큐, 자바 애플리케이션 단에서 코드 동기화를 이용해서 중복 문제를 해결했지만 각 방식 모두 문제점이 존재했습니다. 그래서 최종적으로 Dplanner에서는 Redis를 이용하여 중복 예약 문제를 해결합니다.

Redis 도입 계기

Redis를 도입하게된 계기는 비즈니스 요구사항 중 시간 단위로만 예약이 가능하다는 점에서 착안하였습니다. 예를 들어 특정 리소스 ID(1)에 대해 2024년 3월 16일 13시부터 16시까지 예약을 하려고 합니다. 이때 예약은 00시 00분, 즉 정각만 가능합니다.
따라서 특정 리소스에 대해서 만들 수 있는 예약은 하루에 24개로 한정되기 때문에 충분히 이를 활용하여 키값으로 만들 수 있다고 판단하였습니다.

또한 Redis는 싱글 스레드로 요청을 처리하기 때문에 동시에 여러 요청이 와도 순차적이고 독립적으로 연산을 처리합니다.

따라서 예약 요청을 캐싱하여 중복 예약 문제를 해결하기로 하였습니다.

예약 요청 캐싱 과정

해당 상황을 가정하겠습니다.

특정 리소스 ID(1)에 대해 2024년 3월 16일 13시부터 16시까지 예약을 하려고 합니다.

RedisReservationService.class

  public Boolean saveReservation(LocalDateTime startDateTime, LocalDateTime endDateTime, Long resourceId) {

        int startHour = startDateTime.getHour();
        int endHour = endDateTime.getHour() ;

        if(endHour == 0){
            endHour = 24;
        }
        Map<String, String> map = new HashMap<>();
        for (int i = 0; i < (endHour - startHour); i++) {
            String key = generateKey(startDateTime.plusHours(i),resourceId);
            String value = "r"; // reserved
            map.put(key, value);
        }

        Boolean ret = redisTemplate.opsForValue()
                .multiSetIfAbsent(map);

        if(ret){
            expireReservation(startDateTime, endDateTime, resourceId);
        }

        return ret;

    }

key값 만들기

키값은 리소스ID와 예약시간을 조합하여 만들게 됩니다.

   private String generateKey(LocalDateTime reservedTime, Long resourceId) {

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
        String formattedTime = reservedTime.format(formatter);
        return resourceId.toString() + ":" + formattedTime;
    }

따라서 2024년 3월 16일 13시부터 16시까지 예약을 요청하게 되면 총 3개 키가 만들어지게 됩니다. 1:2024031613,1:2024031614,1:2024031615 이렇게 13시,14시,15시에 해당하는 키값이 만들어집니다.

키값 저장

키값을 저장할 때는 Reids에 MSETNX 연산을 이용합니다. MSETNX는 주어진 키-값 쌍들을 원자적으로 저장합니다. 즉 저장하고자 하는 키가 하나라도 이미 Redis에 저장되어 있다면 해당 연산은 모두 처리되지 않습니다.

MSETNX 연산을 이용하면 요청받은 예약 시간에 대해서 이미 겹치는 요청이 이전에 있었는지 알 수 있습니다.

MSETNX : Sets the given keys to their respective values. MSETNX will not perform any operation at all even if just a single key already exists.

multiSetIfAbsent 은 연산이 정상적으로 처리되면 true를 아니면 false를 반환합니다.

키값 TTL 설정

저장된 키값에 대해 TTL을 설정합니다. TTL은 짧게 1분으로 가져갑니다. 그 이유는 혹시라도 캐시 값이 잘 못 설정되어 예약이 안되더라도 1분 이후에는 예약이 가능하도록 하였습니다.

ReservationService.class

여기서 주의할 점은 Redis로만 중복 예약을 검사하는 것은 아닙니다. Redis는 단순히 예약 요청을 캐싱하여 처리하는 것일 뿐이지 당연히 데이터베이스에 있는 기존에 예약을 조회하여 중복 예약을 검사합니다.

따라서 전체적인 예약 요청을 처리하는 과정은 다음과 같습니다.

  1. 데이터 베이스에서 예약 요청에 해당하는 중복된 예약이 있는지 조회합니다.

  2. 캐시에 예약 요청을 저장
    -> 저장이 불가능 하다는 것은 이미 해당 기간에 예약이 존재하다는 것을 가정

  3. 최종적으로 예약 요청 데이터베이스에 저장

    @Transactional
    public ReservationDto.Response createReservation(Long clubMemberId, ReservationDto.Create createDto) {

        Long resourceId = createDto.getResourceId();
        LocalDateTime startDateTime = createDto.getStartDateTime();
        LocalDateTime endDateTime = createDto.getEndDateTime();
        // 이미 예약이 있는지 검사
        if (reservationRepository.existsBetween(startDateTime, endDateTime, resourceId)) {
            throw new ServiceException(RESERVATION_UNAVAILABLE);
        }

		''' 생략 '''

		// 레디스 확인
        Boolean cache = redisReservationService.saveReservation(startDateTime, endDateTime, resourceId);
        if(!cache){
            throw new ServiceException(RESERVATION_UNAVAILABLE);
        }
        // 예약을 생성합니다.
        reservation = reservationRepository.save(createDto.toEntity(clubMember, resource));
        return ReservationDto.Response.of(reservation);
    }

마무리

중복 예약 문제를 해결하기 위해 다양한 방식을 도입하면서 문제를 해결하는 방식에는 정말 다양한 방식이 있다는 것을 알게되었습니다. 기술적으로도 다양한 방식을 적용해보면서 재밌는 시간이었던 것 같습니다.

그리고 기술적으로 해결하기 어려운 상황이라면, 비즈니스 요구사항을 절충하여 해결할 수 있다는 사실을 알게되었습니다. 해당 포스트에 create하는 부분만 작성하였지만, 기존에는 예약 update을 할 때 시간 또한 수정이 가능하도록 하였습니다. 하지만 중복 예약 문제를 다루면서 시간관련된 처리는 무조건 create할 때만 다루는 것이 문제가 발생할 수 있는 코드를 일원화할 수 있고 추후에 관리하기 좀 더 편리할 것이라고 판단하였고, 사용자 관점에서도 수정할 때 시간 수정이 불가능한 것이 큰 불편함으로 다가오지 않을 것이라는 팀원들의 의견을 종합하여 update할 때는 시간은 불가능하게 하였습니다. 이런식으로 비즈니스 요구사항과 기술적인 부분을 잘 절충하여 문제를 해결할 수 있다는 것을 알게되었습니다.

그리고 추후에는 redis가 성능이나 redis가 down되면 예약이 안되기 때문에 bottle neck이 될 수 있습니다. 따라서 만약을 대비해 replica를 준비해서 master가 down되었을 때도 문제없이 예약 요청을 처리할 수 있도록 해야할 것 같고 scale out 할 때도 resource id로 redis를 바인딩하여 scale out을 도입하는 것도 좋은 방법이 될 것 같습니다.

0개의 댓글