Skip to content

[5주차] rate limit 적용#103

Merged
soonhankwon merged 9 commits intohanghae-skillup:dbdb1114from
dbdb1114:dev
Feb 12, 2025
Merged

[5주차] rate limit 적용#103
soonhankwon merged 9 commits intohanghae-skillup:dbdb1114from
dbdb1114:dev

Conversation

@dbdb1114
Copy link

@dbdb1114 dbdb1114 commented Feb 9, 2025

[5주차] rate limit 적용

guava 이용 rate limit 적용했습니다.
redis 기반 rate limit 적용했습니다.


작업 내용

  • rate limit 적용했습니다.

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

  • lua script가 동작하지 않아서 하루를 통으로 날렸는데, 제가 주석을 잘못된 방식으로 작성해서 안 됐던 것이었습니다....

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

  • rate limit을 적용함에 있어 이 내용을 어떤 관점에서 적용해야 할 지 고민이 많았습니다. 실제로 rate limit이라는 요구사항을 구현해본 게 처음이어서 그랬던 것 같습니다.

리뷰 포인트

  • rate limit을 규격화 하여 사용했습니다. preCheck, postCheck를 가지는 Interface를 선언하여 규격화 하고, 이를 ticket 예매와 상영 정보 조회 Rate limit을 별도로 구현했는데 이런 구조가 적절한지 여쭙고 싶습니다.

기타 질문

  • 제 생각에 rate limit은 비즈니스 로직 보다는 서버의 안정성과 안전성 면에서 필요한 모듈이라 생각했고, 이런 내용은 여러 api에 구현되야 할 수 있다고 생각했습니다. 그래서 규격할 수 있는 부분과 없는 부분으로 나누어 생각했습니다.
  • 규격화 할 수 있는 부분 : 적용 위치, 적용 방식[ 요청 전처리 및 후처리 ]
  • 규격화 할 수 없는 부분 : api별 rate limit 조건 및 구현 방식
    이런 발상으로 코드를 작성했고, api별 RateLimit 클래스를 작성하고, 어노테이션만 추가하면 각 api에 rate limit을 적용할 수 있도록 설계를 해보았는데 이게 RateLimit을 이런식으로 구현해도 되는 건지, 너무 생각을 많이 해서 조금 오바스럽게 구현한 건 아닌지 여쭤보고 싶습니다.

@soonhankwon soonhankwon reopened this Feb 11, 2025
@soonhankwon
Copy link

안녕하세요 정현님 :) 리부트 코스때 뵙고 오랜만이네요!
열심히 해주시고, 제출해주셔서 감사합니다~ 리뷰 포인트 위주로 리뷰 진행하겠습니다


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimitWith {

Choose a reason for hiding this comment

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

RateLimiter를 AOP를 활용해서 구현해주신점 좋습니다 :)

  • default를 활용한 부분도 좋네요 👍

import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

public interface RateLimiter {

Choose a reason for hiding this comment

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

preCheck, postCheck 행위를 "선언 및 추상화"한 부분 인상깊습니다. interface로 추상화할때 주의점은

  • 일반 애플리케이션 개발에서는 "진짜 범용 추상화"라는 것이 거의 없다는 점입니다(변화하기 때문, 라이브러리는 예외).
  • 다만, RateLimiter를 정의하는 구현체가 해당 메서드의 선언을 정확하게 동작하도록 "정의"를 잘하고 있는지?
  • 만약, 어떤 구현체는 postCheck가 필요없어 로깅만 하던지, 의미없는 리턴을 한다면 잘못된 추상화라고 할 수 있습니다 :)

Copy link
Author

Choose a reason for hiding this comment

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

진짜 범용 추상화 라는 것이 없다는 점, 잘못된 추상화에 대해 알려주셔서 너무 감사합니다. 앞으로 프로그래밍을 하며 많이 참고할 것 같습니다. 해당 부분 좀 더 고민하여 더 좋은 코드로 만들어보겠습니다


@Component
@RequiredArgsConstructor
public class ShowingSearchRateLimiter implements module.config.ratelimit.limiters.RateLimiter {

Choose a reason for hiding this comment

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

Lua Script를 활용해 API에 맞는 RateLimiter를 구현해주신 부분 좋습니다 👍

Choose a reason for hiding this comment

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

  • 다만 해당 부분이 "하드코딩"되어 있습니다. 아시겠지만, 버그 발생시 런타임에 오류가 발생 그리고 버그 파악도 어려운 부분이 있습니다.
  • 시간이되신다면 커스텀하게 해당 부분을 책임지는 컴포넌트(클래스)를 만들어 보는 것도 고려할 수 있습니다.(eg QueryDSL)

Copy link
Author

Choose a reason for hiding this comment

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

흥미로운 시도일 것 같습니다!! 시도해보겠습니다! 감사합니다.

String username = ticketReservationRequest.getUsername();

if(isBlocked(username,showingId)){
throw new TwoManyReservationRequestException();

Choose a reason for hiding this comment

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

Two -> Too 오타가 있네요 :)


public boolean isBlocked(String user, Long showingId) {
String key = keyFormatter(user, showingId);
return redisTemplate.opsForValue().get(key) != null;
Copy link

@soonhankwon soonhankwon Feb 11, 2025

Choose a reason for hiding this comment

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

만약 Redis 서버(보통 현업에서는 Caching 서버를 따로 배포해서 활용)가 중간에 문제로 다운된다면? 어떻게 될까요.

  • 주석이라던지, 명확한 가정이 필요해보입니다. 아니라면 Redis 장애에 대비한 동작이 있으면 좋겠습니다.

Copy link
Author

Choose a reason for hiding this comment

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

Redis 도입에 급급했던 것 같습니다. 장애에 대한 대비 해보겠습니다.

String username = request.getUsername();
List<TicketDTO> ticketList = request.getTicketList();
return ResponseEntity.ok(ticketService.reservationWithFunctional(showingId, username, ticketList));
return ResponseEntity.ok(ticketService.reservation(showingId, username, ticketList));

Choose a reason for hiding this comment

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

자바 코딩 스타일 컨벤션은 메서드는 동사를 사용함으로 reserve가 더 맞아보입니다 :)

@ExceptionHandler(TooManyRequestException.class)
@ResponseStatus(HttpStatus.NO_CONTENT)
protected ResponseEntity<String> tooManyRequestException(TooManyRequestException e){
return ResponseEntity.status(HttpStatus.BAD_REQUEST)

Choose a reason for hiding this comment

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

Too many request에 알맞게 429 상태코드를 리턴하도록 해주시면 더 적합할듯 합니다 :)

}

@Test
public void 리미터_51번째_조회() throws Exception {

Choose a reason for hiding this comment

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

RateLimiter에 대한 적절한 테스트코드 좋습니다 👍

@@ -148,15 +168,7 @@ public void validateAndReserve(Long showingId, User user, List<TicketDTO> ticket
int userAge = Period.between(user.getBirth(), LocalDate.now()).getYears();
if (userAge < movie.getRating().getAge())

Choose a reason for hiding this comment

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

비교연산자(if문)는 의미있는 메서드로 Extract 해주면 가독성이 좀 더 향상될것 같습니다. eg) isValidAge

Copy link
Author

Choose a reason for hiding this comment

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

세세한 리뷰 감사드립니다!!ㅠㅠ 이런 한 말씀 한 말씀,, 너무 귀중하네요


public void validateAndReserve(Long showingId, User user, List<TicketDTO> ticketDtoList) {
public void validateReservation(Long showingId, String username, List<TicketDTO> ticketDtoList) {
Optional<User> optionalUser = userRepository.findByUsername(username);

Choose a reason for hiding this comment

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

Optional을 활용한다면 findByUsername에서 바로 throw 해줘서 user 객체를 바로 사용할 수 있도록 간결하게 바꾸는 것도 좋아보입니다 :)


// 최종연산 [ 결제 완료 처리 ]
for (Ticket ticket : ticketList) {
ticket.setTicketStatus(TicketStatus.RESERVED);
Copy link

@soonhankwon soonhankwon Feb 11, 2025

Choose a reason for hiding this comment

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

간단한 로직이지만 객체 내부에서 상태를 바꾸도록 하고, 좀더 의미있는 메서드 이름으로 좀 더 OOP에 맞게 개선할 수 있을것같습니다 :)

@soonhankwon
Copy link

좋았던 점

  • API 구현이 대체적으로 적절해서 좋았습니다.
  • 코드 스타일이 "일관적"이어서 좋습니다.
  • "Ratelimiter"를 잘 구현하고 요구사항에 맞게 잘 적용하셨습니다.
    • RateLimiter의 추상화 및 각각의 API에 맞게 별도의 RateLimiter를 구현해서 적용하신 부분 좋습니다.
  • Presentation Layer 및 RateLimit에 대한 테스트를 해주셨습니다.

아쉬웠던 점

  • RateLimiter 예외의 상태코드가 아쉽습니다.
  • Redis 장애에 대비하는 코드 및 해당 부분에대한 가정 및 주석이 없어서 아쉬웠습니다.

리뷰 포인트

RateLimit을 이런식으로 구현해도 되는 건지, 너무 생각을 많이 해서 조금 오바스럽게 구현한 건 아닌지 여쭤보고 싶습니다

  • 너무 좋은 시도고 인상깊었습니다. 정현님이 많이 고민하신 포인트가 느껴져서 좋습니다.
  • 구현부는 다 적절하고 좋습니다. 하지만 인터페이스로 추상화한 부분은 리뷰에서 말씀드린 것처럼 주의할 부분만 고려하시면 좋겠습니다.

이번 코스 고생많으셨고, 새해복많이받으세요!!

@soonhankwon soonhankwon merged commit d28f453 into hanghae-skillup:dbdb1114 Feb 12, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants