Skip to content

[4주차] RateLimit 적용, 비관락/Redisson 분산락 적용#91

Merged
soonhankwon merged 8 commits intohanghae-skillup:somin-jeongfrom
somin-jeong:main
Feb 4, 2025
Merged

[4주차] RateLimit 적용, 비관락/Redisson 분산락 적용#91
soonhankwon merged 8 commits intohanghae-skillup:somin-jeongfrom
somin-jeong:main

Conversation

@somin-jeong
Copy link

[4주차] RateLimit 적용, 비관락/Redisson 분산락 적용


작업 내용

비관락 구현

Seat 테이블에 좌석 데이터를 넣고, Seat 테이블에서 좌석을 조회할 때 락을 걸었습니다.
그리고 ReservationSeat과 Reservation 테이블에 예약 정보를 저장했습니다.
첫 번째 트랜잭션이 커밋된 후 락이 풀리면 그 다음 트랜잭션이 좌석에 대한 락을 얻게 되고, ReservationSeat 테이블에 좌석이 예약되어 있는지 조회할 때 락을 걸었습니다.

함수형 분산락 구현

Redisson MultiLock을 사용해서 모든 좌석의 락을 한꺼번에 획득해야 예약을 진행할 수 있도록 하였습니다.
MultiLock을 적용하기 위해 좌석 별로 락 키를 생성했습니다.

Guava RateLimiter

Guava RateLimiter를 사용해서 초당 2개의 요청만 허용하도록 구현했습니다.

Redisson RateLimiter

조회 API 에 Redisson RateLimit 을 적용하기 위해 1분 내 50회 이상 요청 시 1시간 동안 해당 IP 를 차단하는 Lua script를 작성해서 구현했습니다.

발생했던 문제와 해결 과정을 남겨 주세요.

  • 문제 1 - Seat 테이블에만 비관락을 적용했을 때 동시성 제어가 성공하지 않는 문제 발생
  • 해결 방법 1 - ReservationSeat 테이블에도 락을 걸어서 해결했습니다. 트랜잭션 격리 수준 REPEATABLE READ은 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정하더라도 동일한 결과를 반환할 것을 보장하기 때문입니다. 따라서 트랜잭션이 시작되었을 때 ReservationSeat 데이터를 유지하기 때문에 첫 번째 트랜잭션이 ReservationSeat 테이블에 예약 좌석 데이터를 추가하더라도 다른 트랜잭션들은 알 수 없었던 것입니다. ReservationSeat 테이블에 락을 걸어줌으로써 ReservationSeat 테이블의 최신 데이터를 받아올 수 있게 되었습니다.

이번 주차에서 고민되었던 지점이나, 어려웠던 점을 알려 주세요.

과제를 해결하며 특히 어려웠던 점이나 고민되었던 지점이 있다면 남겨주세요.

리뷰 포인트

리뷰어가 특히 의견을 주었으면 하는 부분이 있다면 작성해 주세요.

ex) Redis 락 설정 부분의 타임아웃 값이 적절한지 의견을 여쭙고 싶습니다.

기타 질문

추가로 질문하고 싶은 내용이 있다면 남겨주세요.

ex) 테스트 환경에서 동시성 테스트를 수행하였고, 모든 케이스를 통과했습니다. 추가할 테스트 시나리오가 있을까요?

@soonhankwon
Copy link

안녕하세요 소민님 😃
4주차 열심히 참여해주셔서 감사합니다. 리뷰 진행하겠습니다 :)

// IP 차단 여부 확인
Long allowed = redisRateLimiterService.isAllowed(clientIp);
if (allowed == -1) {
throw new RateLimitExceededException(TOO_MANY_REQUEST_ERROR);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조회 API에 RateLimit 적용 및 429 응답을 주는점 좋습니다 👍

@Slf4j
@Service
@AllArgsConstructor
public class RedisRateLimiterService {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis를 활용해서 rateLimit 요구사항 구현이 잘된점이 좋네요 👍

}
// Redisson MultiLock 적용 (여러 개의 좌석을 동시에 보호)
redissonLockUtil.executeWithMultiLock(lockKeys, 5, 10, () -> {
saveReservationWithTransaction(reservationRequestDto, reservationSeats, screenRoomId);
Copy link

@soonhankwon soonhankwon Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

락을 획득한 요청만 Transactional이 적용되도록 설계하신점 좋습니다 👍
다만, "스프링 특성"상 메서드에서 같은 클래스의 메서드를 호출할때 "애노테이션"이 적용되지 않습니다.

  • 해당 문제일때 해결하는 방법을 찾아보시면 좋은 경험이 될 듯 합니다.

throw new SeatException(ALREADY_RESERVED_SEAT_ERROR);
}
}
private void validateUserReserveSeats(List<SeatsDto> reservationSeats, Long userId, Long screenScheduleId) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API 구현시 꼼꼼하게 Validation 진행하신 점이 인상깊습니다 👍

@SpringBootTest
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presentation Layer의 통합테스트 작성 좋네요 👍

}
}

public static void containsReservedSeat(List<SeatsDto> reservationSeats, List<SeatsDto> seatsDtoByUserId) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재의 반복문에서 stream을 사용하면 가독성을 향상시킬 수 있습니다 :)

public static void validateContinuousSeats(List<SeatsDto> seats) {
seats.sort(Comparator.comparing(seat -> seat.getCol().getColumn()));

for (int i = 1; i < seats.size(); i++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복문에서 리스트의 size()메서드를 반복호출하는것은 반복횟수가 많아질수록 성능에 좋지않습니다.

  • 불필요한 습관은 굳이 사용하지 않는 편이 좋다고 생각합니다 :)

@soonhankwon
Copy link

soonhankwon commented Feb 4, 2025

좋았던 점

  • API 구현(응답 스펙)이 대체적으로 적절하고, Validation을 꼼꼼히 신경쓴 점이 아주 좋습니다.
  • Ratelimit를 잘 구현하고 요구사항에 맞게 적용하셨습니다.
  • 적절한 HTTP 상태코드를 응답(429)하고, 커스텀한 포맷으로 잘 응답이 반환되어 좋습니다.
  • Presentation Layer에서 통합 테스트를 잘 작성하시고, RateLimit에 대한 테스트를 해주셨습니다.

아쉬웠던 점

  • 테스트 커버리지에대한 내용이 없어서 아쉬웠습니다.
  • 락을 획득한 요청만 Transactional을 적용하시려 하신점이 좋지만, 스프링의 특성을 좀더 이해할 필요가 있습니다.
  • Domain Layer에 대한 테스트가 보이지 않아서 아쉽습니다.
  • Stream을 사용하여 가독성을 훨씬 향상시킬수 있는 부분들이 다수 존재했습니다.

@soonhankwon soonhankwon merged commit f5877cd into hanghae-skillup:somin-jeong Feb 4, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants