Skip to content

[4주차] RateLimit 적용 및 테스트 코드 작성#96

Merged
pyuseon merged 46 commits intohanghae-skillup:orkrj2from
orkrj:4thweek
Feb 4, 2025
Merged

[4주차] RateLimit 적용 및 테스트 코드 작성#96
pyuseon merged 46 commits intohanghae-skillup:orkrj2from
orkrj:4thweek

Conversation

@orkrj
Copy link

@orkrj orkrj commented Feb 3, 2025

조회 및 예매 로직에 RateLimit 적용, 조회 RateLimiting 테스트 작성, Jacoco 설정 추가


작업 내용

  1. 함수형 분산 락 구현 및 부하 테스트 진행
  • AOP 기반 분산 락과 함수형 분산 락을 테스트한 결과, AOP 분산 락의 성능이 더 우수하여 AOP 기반으로 최종 구현하였습니다.
  1. 영화 조회를 1분에 50회로 제한
  • /api/v1/movie 엔드포인트에 대해 1분 동안 조회 횟수가 50회를 초과하면 해당 IP를 차단하는 로직을 추가했습니다.
  • Redisson의 RAtomicLong, RCacheMap 을 활용하였으며, 서블릿 필터를 사용하여 요청이 비즈니스 로직에 도달하기 전에 차단하도록 했습니다.
  • 필터를 사용한 이유: 서블릿 단계에서 요청을 차단하면, 불필요한 비즈니스 로직 수행을 방지하여 성능을 최적화할 수 있습니다.
  • 간단한 RateLimit 테스트를 추가했습니다.
  1. 예매 성공 후 5분 동안 추가 예매 제한
  • 비즈니스 로직 내에서 직접 RateLimit을 적용했습니다.
  • 예매 성공 시, Redisson을 활용하여 요청자의 IP 기반 Key를 저장합니다.
  • 이후 해당 Key가 존재하는 경우 예매 요청을 차단하여 추가적인 예매를 방지합니다.
  • 예매 요청이 들어오면 DB 저장 전에 캐시에서 제한 여부를 확인하고, 제한 대상일 경우 예매 로직을 롤백하도록 구현했습니다.
  • 기존의 여러 제한 정책(예: 1인당 최대 좌석 수 제한, 좌석 연속성 검증 등)과 함께 예매 서비스 내에서 처리하여 응집도를 높였습니다.
  1. Jacoco 의존성 추가 및 테스트(진행 중)
  • Jacoco 의존성을 추가했으나, 현재 커버리지가 정상적으로 집계되지 않는 문제가 있어 추가 조치가 필요합니다.

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

  1. 테스트 설계의 어려움
  • 단순한 단위 테스트는 어렵지 않지만, 실제 환경에서 어떻게 테스트할 것인지를 고민하는 과정에서 시간이 많이 소요되었습니다.
  1. 해결 방법이 너무 많음
  • Redis의 자료구조 선택부터, RateLimit 적용 방식(AOP vs. 함수형), 락 구현 방식 등 해결책이 너무 다양했습니다.
  • 특히, 비즈니스 로직 내에서 직접 RateLimit을 적용할 것인지, 또는 서블릿 필터에서 차단할 것인지를 결정하는 과정이 어려웠습니다.

리뷰 포인트

  1. 함수형 분산 락 구현 관련
  • 일반적으로 함수형 분산 락이 성능 면에서 유리하다고 알려져 있지만, 테스트 결과는 반대로 나왔습니다.
  • 혹시 제가 함수형 분산 락을 구현한 방식에 문제가 있었을 가능성이 있는지 궁금합니다.
  • (ReservationServiceImpl의 reserveSeatWithLamda() 메서드에서 DistributedLockFunction을 사용했습니다.)
  • (락 구현은 InfraStructure 내 common/functional/DistributedLockFunction 에 정의되어 있습니다.)
  1. 예매 로직의 RateLimit 구현 방식
  • 처음에는 애플리케이션 레이어에서 직접 RedissonClient를 의존하게 만들려고 했습니다.
  • 하지만 이렇게 하면 모듈 간 경계가 모호해지고, 결국 포트 앤 어댑터 패턴을 적용하여 인프라 모듈에서 이를 구현하도록 변경했습니다.
  • 문제는 이렇게 구현하니, 애플리케이션 모듈과 인프라 모듈이 순환 참조되는 문제가 발생했습니다.
  • 결국 도메인 포트에 인터페이스를 만들고, 이를 인프라 모듈에서 구현하는 방식으로 해결했지만, 이 방식이 올바른 선택인지 궁금합니다.
  • 즉, 어플리케이션 모듈이 포트로서 서비스 인터페이스를 관리하는데, 도메인 모듈도 포트 패키지 내 서비스 인터페이스를 가지는 게 어색하진 않는지 궁금합니다.

기타 질문

  1. Redis의 RAtomicLong vs. RCacheMap
  • 영화 조회 로직의 RateLimit을 위해 RAtomicLong을 사용했습니다.
  • 하지만 RAtomicLong은 개별 키 값을 저장하는 방식이라 관리 측면에서 불편한 점이 있었습니다.
  • 반면, RCacheMap은 키 값을 한 곳에서 관리할 수 있지만 redis-cli 로 TTL 을 조회할 수 없어 불편했습니다.
  • 실무에서는 특히 선호되는 자료 구조가 있는지 궁금합니다.
  1. 실무에서의 테스트
  • 단위 테스트는 작성하고 있지만, 통합 테스트, 부하 테스트, Jacoco 커버리지 분석까지 하려니 부담이 상당했습니다.
  • 다양한 방식들이 있겠지만, 실무에서는 어느 수준까지 테스트하는 것이 일반적인지, 현업에서의 테스트 이야기를 조금 듣고 싶습니다..!

orkrj added 30 commits January 27, 2025 04:41
@pyuseon
Copy link
Contributor

pyuseon commented Feb 4, 2025

안녕하세요 우준님 ! 이번주도 고생 많으셨습니다!
리뷰 시작 하도록 할게요!

Comment on lines +67 to +87
return distributedLockFunction.executeFunctionalLock(
key,
waitSeconds,
leaseSeconds,
() -> {
Reservation reservation = initReservation(request);
List<Seat> seats = getSeats(request);

List<ScheduleSeat> scheduleSeats = getScheduleSeats(request, seats);
checkDoubleBooking(scheduleSeats);

List<ReservationSeat> reservationSeats = toReservationSeats(reservation, seats);
checkReservationPolicy(reservation, reservationSeats, MAX_SEATS_PER_RESERVATION);

reservation.setReservationSeats(reservationSeats);
scheduleSeats.forEach(scheduleSeat -> scheduleSeat.setReserved(true));

return ReservationResponse.from(reservationRepository.reserve(reservation));
}
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 이 부분에 함수형 분산락을 걸어주셨는데요.
락이 필요 없는 데이터 조회 및 검증 로직이 포함되어 있다면, 락을 걸기 전에 실행하는 것이 좋습니다.

  • 좌석 정보를 조회하는 부분(getSeats, getScheduleSeats)은 락을 걸지 않아도 되는 연산이므로, 락을 걸기 전에 수행하는 것이 좋습니다.
  • DB 업데이트(쓰기 작업)만 락이 적용된 상태에서 실행하도록 수정하면 락 점유 시간이 줄어들어 성능이 개선될 수 있습니다.
  • 임계영역을 최소화하면 대기 시간이 감소하여, 성능 테스트 결과에도 영향을 미칠 수 있어요!

return ReservationResponse.from(reservationResult);
}

@Transactional
Copy link
Contributor

Choose a reason for hiding this comment

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

현재 메소드 전체에 Transactional을 사용해 주셔서 락을 획득하기 전에 트랜잭션이 시작될 가능 성이 있습니다!
트랙잭선을 락 내부에서 실행하도록 변경해 주는 것이 좋습니다!

throw new RuntimeException("Lock interrupted: " + e.getMessage());
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
Copy link
Contributor

Choose a reason for hiding this comment

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

락을 해제할 때에도 exception이 발생할 수 있는데요. 이때 처리를 따로 해주지 않는다면 락이 풀리지 않을 수도 있겠죠.
사용하고 계신 Redisson에서는 IllegalMonitorStateException이 발생할 수도 있어서요.
try-catch 로 안전하게 해제 하시는 것을 추천 드립니다.

Comment on lines +3 to +5
public interface ReservationRateLimitChecker {

void oneReservationPerFiveMinutes(String key);
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분에서 순환 참조를 해결하기 위해 인터페이스를 도메인 모듈에 추가 하신 것으로 보이는데요.
현재 적용한 방식은 아키텍쳐를 흔들지 않고 도메인과 인프라의 경계를 잘 구분하신 것으로 보이네요!

}

@Test
public void testRateLimit() throws ServletException, IOException {
Copy link
Contributor

Choose a reason for hiding this comment

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

RateLimit 테스트를 잘 구현해 주셨네요!
시간이 되신다면 동시 요청이 있을때의 RateLimiter 적용이 정상적으로 작동하는지 확인하는 테스트도 추가하시는 것이 좋아요!

// 51번 째 요청
response = new MockHttpServletResponse();
rateLimitFilter.doFilter(request, response, filterChain);
assertEquals(429, response.getStatus(), "51st request should be blocked");
Copy link
Contributor

Choose a reason for hiding this comment

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

status 코드를 적절하게 구성해 주셨네요!

Copy link
Contributor

Choose a reason for hiding this comment

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

현재 RAtomicLong을 사용하고 계신데, 개별 키 값을 관리해야 하기 때문에 유지보수 측면에서 불편함이 있을 수 있습니다.
이를 개선하기 위해 RMapCache를 사용하면 한 곳에서 IP 요청 수를 관리할 수 있어 더 나은 방법으로 보입니다.
다만, Redis CLI에서 TTL을 직접 조회하기 어렵다는 단점이 있는데, 이를 보완하기 위해 TTL을 별도로 저장하는 RMapCache를 추가하면 TTL 조회가 가능할 것 같습니다.

@pyuseon
Copy link
Contributor

pyuseon commented Feb 4, 2025

좋았던점

  • 초반에 설계한 아키텍쳐를 유지하면서 RateLimiter를 적용하시기 위해 노력한게 느껴졌습니다. 전반적인 코드도 깔끔했습니다.
  • 지난 주차 내용까지 포함해서 구현하기 쉽지 않은 분량이었는데 분산락 두가지를 구현 및 성능테스트까지 해주신점인 좋았습니다.

아쉬운점

  • 테스트 코드를 아직 작성중이신 것으로 보이는데요 통합 테스트 코드도 꼭 작성해 보시는 것을 추천드려요.
  • Lua Script를 작성하여 RateLimiter를 구현하시는 것도 해보시면 좋을 것 같습니다!

리뷰 포인트

-> 코드리뷰에 작성해 두었습니다. 😄

기타 질문

단위 테스트는 작성하고 있지만, 통합 테스트, 부하 테스트, Jacoco 커버리지 분석까지 하려니 부담이 상당했습니다.
다양한 방식들이 있겠지만, 실무에서는 어느 수준까지 테스트하는 것이 일반적인지, 현업에서의 테스트 이야기를 조금 듣고 싶습니다..!

  • 회사의 관점에서는 최종적으로 통합 테스트가 제대로 작동하는지가 가장 중요한 것 같습니다. 서비스를 런칭하기 전에 오류 없이 전반적인 기능이 정상적으로 동작하는지를 검증해야 하기 때문입니다. 이를 위해 단위 테스트를 꼼꼼하게 작성해 주요 기능이 잘 동작하는지를 미리 확인합니다. 일반적인 테스트 방식은 팀마다 다를 수 있지만, 핵심 기능이 정상적으로 수행되는지 확인하는 것이 가장 중요하다고 생각합니다. 결국, 이러한 테스트 과정이 테스트 커버리지와 연결된다고 볼 수 있습니다. 🔍

4주 동안 정말 고생 많으셨습니다!
과제를 구현 하면서 고려할 점이 많았을 텐데도 짧은 시간 안에 완성도를 높이기 위해 노력하신 점이 느껴졌습니다!
아직 구현되지 않은 사항들도 같이 구현해 보시면 프로젝트 완성도에 도움이 될거에요! 마지막 까지 화이팅입니다! 💪

@pyuseon pyuseon merged commit 1bb871c into hanghae-skillup:orkrj2 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