From aadfab0c31ebfea4e5f456ff959015acfdf5796b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:25:43 +0900 Subject: [PATCH 01/29] =?UTF-8?q?build:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/build.gradle b/backend/build.gradle index bd3a976cf..cea87d9cf 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' From 0c918217c95a8c047ec100cec34b235eda6f3de1 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:26:35 +0900 Subject: [PATCH 02/29] =?UTF-8?q?chore:=20=EA=B0=9C=EB=B0=9C=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20redis=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 45df6e2cb..3b9858a83 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -37,3 +37,7 @@ cors: allowed-origins: - http://localhost - https://localhost + +redis: + host: localhost + port: 6379 \ No newline at end of file From cc5253a7a0442d36c9278f95950c02723c21faa0 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:27:20 +0900 Subject: [PATCH 03/29] =?UTF-8?q?feat:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=84=20=EB=B9=88=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reviewme/config/RedisConfig.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/src/main/java/reviewme/config/RedisConfig.java diff --git a/backend/src/main/java/reviewme/config/RedisConfig.java b/backend/src/main/java/reviewme/config/RedisConfig.java new file mode 100644 index 000000000..38d590d12 --- /dev/null +++ b/backend/src/main/java/reviewme/config/RedisConfig.java @@ -0,0 +1,31 @@ +package reviewme.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +public class RedisConfig { + + @Value("${redis.host}") + private String host; + + @Value("${redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } +} From 20e266a766ee16a23cf61f980c5e148b001060d5 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:29:26 +0900 Subject: [PATCH 04/29] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=B2=98=EB=A6=AC=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - URI, IP, UserAgent 를 기준으로 1초에 3개보다 많은 요청이 오는 경우, 429를 응답한다. 즉, 4번째 요청부터 거부된다. --- .../global/DuplicateRequestInterceptor.java | 57 +++++++++++++++++++ .../RequestFrequencyNonNumericException.java | 12 ++++ .../TooManyDuplicateRequestException.java | 12 ++++ 3 files changed, 81 insertions(+) create mode 100644 backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java create mode 100644 backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java create mode 100644 backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java new file mode 100644 index 000000000..fb6cd9a96 --- /dev/null +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -0,0 +1,57 @@ +package reviewme.global; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import reviewme.global.exception.RequestFrequencyNonNumericException; +import reviewme.global.exception.TooManyDuplicateRequestException; + +@Component +@RequiredArgsConstructor +public class DuplicateRequestInterceptor implements HandlerInterceptor { + + private static final int MAX_FREQUENCY = 3; + private static final Duration DURATION = Duration.of(1, ChronoUnit.SECONDS); + + private final RedisTemplate redisTemplate; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!HttpMethod.POST.matches(request.getMethod())) { + return true; + } + + String key = generateRequestKey(request); + Object value = redisTemplate.opsForValue().get(key); + + if (value == null) { + redisTemplate.opsForValue().set(key, 1, DURATION); + return true; + } + + if (!(value instanceof Integer)) { + throw new RequestFrequencyNonNumericException(value); + } + + int frequency = (int) value; + if (frequency > MAX_FREQUENCY) { + throw new TooManyDuplicateRequestException(key); + } + redisTemplate.opsForValue().set(key, frequency + 1); + return true; + } + + private String generateRequestKey(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + String remoteAddr = request.getRemoteAddr(); + String userAgent = request.getHeader("User-Agent"); + + return String.format("RequestURI: %s, RemoteAddr: %s, UserAgent: %s", requestURI, remoteAddr, userAgent); + } +} diff --git a/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java b/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java new file mode 100644 index 000000000..94c0b1476 --- /dev/null +++ b/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java @@ -0,0 +1,12 @@ +package reviewme.global.exception; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RequestFrequencyNonNumericException extends DataInconsistencyException { + + public RequestFrequencyNonNumericException(Object requestFrequency) { + super("서버 내부 예외가 발생했어요."); + log.error("Request frequency is not numeric - actualType: {}", requestFrequency.getClass().getSimpleName()); + } +} diff --git a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java new file mode 100644 index 000000000..3552f95ae --- /dev/null +++ b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java @@ -0,0 +1,12 @@ +package reviewme.global.exception; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TooManyDuplicateRequestException extends ReviewMeException { + + public TooManyDuplicateRequestException(String requestKey) { + super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); + log.info("Too many duplicate request arrived: {}", requestKey); + } +} From d06ff0f840603ed8fd24a78037a5f321594a87d7 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:29:49 +0900 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=B2=98=EB=A6=AC=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/reviewme/config/WebConfig.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java index d855040f0..8057382b5 100644 --- a/backend/src/main/java/reviewme/config/WebConfig.java +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -3,19 +3,29 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import reviewme.reviewgroup.controller.ReviewGroupSessionResolver; import reviewme.reviewgroup.service.ReviewGroupService; +import reviewme.global.DuplicateRequestInterceptor; +import reviewme.global.HeaderPropertyArgumentResolver; @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final ReviewGroupService reviewGroupService; + private final RedisTemplate redisTemplate; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new ReviewGroupSessionResolver(reviewGroupService)); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new DuplicateRequestInterceptor(redisTemplate)); + } } From d9eb2d30da1c1be6d47851f164e65214d2eab3ec Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:30:08 +0900 Subject: [PATCH 06/29] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/reviewme/global/GlobalExceptionHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index 7724dd90e..4e17d30b3 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -22,6 +22,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import reviewme.global.exception.BadRequestException; import reviewme.global.exception.DataInconsistencyException; +import reviewme.global.exception.TooManyDuplicateRequestException; import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; @@ -50,6 +51,11 @@ public ProblemDetail handleDataConsistencyException(DataInconsistencyException e return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getErrorMessage()); } + @ExceptionHandler(TooManyDuplicateRequestException.class) + public ProblemDetail handleDuplicateRequestException(TooManyDuplicateRequestException ex) { + return ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS, ex.getErrorMessage()); + } + @ExceptionHandler(Exception.class) public ProblemDetail handleException(Exception ex) { log.error("Internal server error has occurred", ex); From b5fc73dfb4880583be7765109a89075fe25c5350 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:30:54 +0900 Subject: [PATCH 07/29] =?UTF-8?q?test:=20=EC=A4=91=EB=B3=B5=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=9D=B8=ED=84=B0=EC=85=89?= =?UTF-8?q?=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DuplicateRequestInterceptorTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java new file mode 100644 index 000000000..c00214a29 --- /dev/null +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -0,0 +1,99 @@ +package reviewme.global; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import reviewme.global.exception.RequestFrequencyNonNumericException; +import reviewme.global.exception.TooManyDuplicateRequestException; + +class DuplicateRequestInterceptorTest { + + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final RedisTemplate redisTemplate = mock(RedisTemplate.class); + private final ValueOperations valueOperations = mock(ValueOperations.class); + private final DuplicateRequestInterceptor interceptor = new DuplicateRequestInterceptor(redisTemplate); + private final String requestKey = "RequestURI: /api/v2/reviews, RemoteAddr: localhost, UserAgent: Postman"; + + @BeforeEach + void setUp() { + given(request.getMethod()).willReturn("POST"); + + given(request.getRequestURI()).willReturn("/api/v2/reviews"); + given(request.getRemoteAddr()).willReturn("localhost"); + given(request.getHeader("User-Agent")).willReturn("Postman"); + + given(redisTemplate.opsForValue()).willReturn(valueOperations); + } + + @Test + void POST_요청이_아니면_통과한다() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + given(request.getMethod()).willReturn("GET"); + + // when + boolean result = interceptor.preHandle(request, null, null); + + // then + assertThat(result).isTrue(); + } + + @Test + void 특정_POST_요청이_처음인_경우_빈도를_1로_초기화한다() { + // given + given(valueOperations.get(anyString())).willReturn(null); + + // when + boolean result = interceptor.preHandle(request, null, null); + + // then + assertThat(result).isTrue(); + verify(valueOperations).set(requestKey, 1, Duration.of(1, ChronoUnit.SECONDS)); + } + + @Test + void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { + // given + int frequency = 1; + given(valueOperations.get(anyString())).willReturn(frequency); + + // when + boolean result = interceptor.preHandle(request, null, null); + + // then + assertThat(result).isTrue(); + verify(valueOperations).set(requestKey, frequency + 1); + } + + @Test + void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { + // given + int maxFrequency = 3; + given(valueOperations.get(anyString())).willReturn(maxFrequency + 1); + + // when & then + assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) + .isInstanceOf(TooManyDuplicateRequestException.class); + } + + @Test + void 특정_POST_요청이_처음이_아니며_빈도가_숫자가_아닌_경우_예외를_발생시킨다() { + // given + given(valueOperations.get(anyString())).willReturn("1"); + + // when & then + assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) + .isInstanceOf(RequestFrequencyNonNumericException.class); + } +} From 5008973535743e9ea02d44105c252ccf0d10622e Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 12:49:06 +0900 Subject: [PATCH 08/29] =?UTF-8?q?refactor:=20value=EB=A5=BC=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=ED=95=A0=20=EB=95=8C=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=EA=B0=84=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reviewme/global/DuplicateRequestInterceptor.java | 4 ++-- .../java/reviewme/global/DuplicateRequestInterceptorTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index fb6cd9a96..9a1895363 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -40,10 +40,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } int frequency = (int) value; - if (frequency > MAX_FREQUENCY) { + if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); } - redisTemplate.opsForValue().set(key, frequency + 1); + redisTemplate.opsForValue().set(key, frequency + 1, DURATION); return true; } diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index c00214a29..97f4f391a 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -73,7 +73,7 @@ void setUp() { // then assertThat(result).isTrue(); - verify(valueOperations).set(requestKey, frequency + 1); + verify(valueOperations).set(requestKey, frequency + 1, Duration.of(1, ChronoUnit.SECONDS)); } @Test From 8544e68313771bd3423d3c15e30d09f860f6d457 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Mon, 7 Oct 2024 16:04:32 +0900 Subject: [PATCH 09/29] =?UTF-8?q?style:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B0=9C=ED=96=89=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/reviewme/global/DuplicateRequestInterceptor.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index 9a1895363..618792b63 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -29,7 +29,6 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String key = generateRequestKey(request); Object value = redisTemplate.opsForValue().get(key); - if (value == null) { redisTemplate.opsForValue().set(key, 1, DURATION); return true; @@ -38,7 +37,6 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if (!(value instanceof Integer)) { throw new RequestFrequencyNonNumericException(value); } - int frequency = (int) value; if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); From 5386fc6749dab468547e23e06ba8d5658ae6f89c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 08:31:24 +0900 Subject: [PATCH 10/29] =?UTF-8?q?style:=20=ED=8C=8C=EC=9D=BC=20=EB=81=9D?= =?UTF-8?q?=20=EA=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 3b9858a83..9c7461a74 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -40,4 +40,4 @@ cors: redis: host: localhost - port: 6379 \ No newline at end of file + port: 6379 From c6e0288a47ab82f3b11ac5a53b0d1dbf59f68ed0 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 08:31:46 +0900 Subject: [PATCH 11/29] =?UTF-8?q?refactor:=20ConfigurationProperties=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/reviewme/config/RedisConfig.java | 13 ++++++------- .../main/java/reviewme/config/RedisProperties.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/reviewme/config/RedisProperties.java diff --git a/backend/src/main/java/reviewme/config/RedisConfig.java b/backend/src/main/java/reviewme/config/RedisConfig.java index 38d590d12..e390bf14d 100644 --- a/backend/src/main/java/reviewme/config/RedisConfig.java +++ b/backend/src/main/java/reviewme/config/RedisConfig.java @@ -1,6 +1,7 @@ package reviewme.config; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -8,17 +9,15 @@ import org.springframework.data.redis.core.RedisTemplate; @Configuration +@EnableConfigurationProperties(RedisProperties.class) +@RequiredArgsConstructor public class RedisConfig { - @Value("${redis.host}") - private String host; - - @Value("${redis.port}") - private int port; + private final RedisProperties redisProperties; @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(host, port); + return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port()); } @Bean diff --git a/backend/src/main/java/reviewme/config/RedisProperties.java b/backend/src/main/java/reviewme/config/RedisProperties.java new file mode 100644 index 000000000..9e06e8715 --- /dev/null +++ b/backend/src/main/java/reviewme/config/RedisProperties.java @@ -0,0 +1,10 @@ +package reviewme.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "redis") +public record RedisProperties( + String host, + int port +) { +} From ad41a00897707aa3b9d9ac477a9f2901e2c5fbe2 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 08:43:02 +0900 Subject: [PATCH 12/29] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reviewme/global/DuplicateRequestInterceptor.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index 618792b63..e5f1733f2 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -3,7 +3,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.time.Duration; -import java.time.temporal.ChronoUnit; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; @@ -17,7 +16,7 @@ public class DuplicateRequestInterceptor implements HandlerInterceptor { private static final int MAX_FREQUENCY = 3; - private static final Duration DURATION = Duration.of(1, ChronoUnit.SECONDS); + private static final Duration DURATION_SECOND = Duration.ofSeconds(1); private final RedisTemplate redisTemplate; @@ -30,7 +29,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String key = generateRequestKey(request); Object value = redisTemplate.opsForValue().get(key); if (value == null) { - redisTemplate.opsForValue().set(key, 1, DURATION); + redisTemplate.opsForValue().set(key, 1, DURATION_SECOND); return true; } @@ -41,7 +40,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); } - redisTemplate.opsForValue().set(key, frequency + 1, DURATION); + redisTemplate.opsForValue().set(key, frequency + 1, DURATION_SECOND); return true; } From 1b0800c68ee92c06aab2ac7a98af7ed0d9fd465d Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 09:05:58 +0900 Subject: [PATCH 13/29] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/reviewme/global/DuplicateRequestInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index e5f1733f2..7f490b651 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -47,7 +47,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons private String generateRequestKey(HttpServletRequest request) { String requestURI = request.getRequestURI(); String remoteAddr = request.getRemoteAddr(); - String userAgent = request.getHeader("User-Agent"); + String userAgent = request.getHeader(USER_AGENT); return String.format("RequestURI: %s, RemoteAddr: %s, UserAgent: %s", requestURI, remoteAddr, userAgent); } From 650d7458f8747880871d016c5dc09c9ec795336b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 09:13:43 +0900 Subject: [PATCH 14/29] =?UTF-8?q?refactor:=20RedisTemplate=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EA=B3=BC=20=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B0=90=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedisTemplate -> RedisTemplate --- .../main/java/reviewme/config/RedisConfig.java | 4 ++-- .../global/DuplicateRequestInterceptor.java | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/reviewme/config/RedisConfig.java b/backend/src/main/java/reviewme/config/RedisConfig.java index e390bf14d..42b4c4ed9 100644 --- a/backend/src/main/java/reviewme/config/RedisConfig.java +++ b/backend/src/main/java/reviewme/config/RedisConfig.java @@ -21,8 +21,8 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + public RedisTemplate requestFrequencyRedisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); return redisTemplate; diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index 7f490b651..239e7de1a 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -1,5 +1,7 @@ package reviewme.global; +import static org.springframework.http.HttpHeaders.USER_AGENT; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.time.Duration; @@ -8,7 +10,6 @@ import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import reviewme.global.exception.RequestFrequencyNonNumericException; import reviewme.global.exception.TooManyDuplicateRequestException; @Component @@ -18,7 +19,7 @@ public class DuplicateRequestInterceptor implements HandlerInterceptor { private static final int MAX_FREQUENCY = 3; private static final Duration DURATION_SECOND = Duration.ofSeconds(1); - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { @@ -27,20 +28,17 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } String key = generateRequestKey(request); - Object value = redisTemplate.opsForValue().get(key); - if (value == null) { - redisTemplate.opsForValue().set(key, 1, DURATION_SECOND); + Long frequency = redisTemplate.opsForValue().get(key); + if (frequency == null) { + redisTemplate.opsForValue().set(key, 1L, DURATION_SECOND); return true; } - if (!(value instanceof Integer)) { - throw new RequestFrequencyNonNumericException(value); - } - int frequency = (int) value; if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); } - redisTemplate.opsForValue().set(key, frequency + 1, DURATION_SECOND); + + redisTemplate.opsForValue().increment(key); return true; } From 953979d7a3f3e9c97c1bddd65204ed4126d5dece Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 09:19:29 +0900 Subject: [PATCH 15/29] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/TooManyDuplicateRequestException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java index 3552f95ae..ce5af45b4 100644 --- a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java +++ b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java @@ -7,6 +7,6 @@ public class TooManyDuplicateRequestException extends ReviewMeException { public TooManyDuplicateRequestException(String requestKey) { super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); - log.info("Too many duplicate request arrived: {}", requestKey); + log.info("Too many duplicate request received: {}", requestKey); } } From fb1acdd200ba6d2d473a1e6b8a367f7a9d015988 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 09:20:41 +0900 Subject: [PATCH 16/29] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=88=EC=99=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RequestFrequencyNonNumericException.java | 12 ------------ .../global/DuplicateRequestInterceptorTest.java | 10 ---------- 2 files changed, 22 deletions(-) delete mode 100644 backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java diff --git a/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java b/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java deleted file mode 100644 index 94c0b1476..000000000 --- a/backend/src/main/java/reviewme/global/exception/RequestFrequencyNonNumericException.java +++ /dev/null @@ -1,12 +0,0 @@ -package reviewme.global.exception; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class RequestFrequencyNonNumericException extends DataInconsistencyException { - - public RequestFrequencyNonNumericException(Object requestFrequency) { - super("서버 내부 예외가 발생했어요."); - log.error("Request frequency is not numeric - actualType: {}", requestFrequency.getClass().getSimpleName()); - } -} diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index 97f4f391a..9d8f05ed9 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -86,14 +86,4 @@ void setUp() { assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) .isInstanceOf(TooManyDuplicateRequestException.class); } - - @Test - void 특정_POST_요청이_처음이_아니며_빈도가_숫자가_아닌_경우_예외를_발생시킨다() { - // given - given(valueOperations.get(anyString())).willReturn("1"); - - // when & then - assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) - .isInstanceOf(RequestFrequencyNonNumericException.class); - } } From a57be0f13474ae3dbc39344b3d417cd79a32da36 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 09:35:36 +0900 Subject: [PATCH 17/29] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - info -> warn --- .../global/exception/TooManyDuplicateRequestException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java index ce5af45b4..4ebf1235b 100644 --- a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java +++ b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java @@ -7,6 +7,6 @@ public class TooManyDuplicateRequestException extends ReviewMeException { public TooManyDuplicateRequestException(String requestKey) { super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); - log.info("Too many duplicate request received: {}", requestKey); + log.warn("Too many duplicate request received: {}", requestKey); } } From ca3df64487303e02755a508ba008d00a78558d87 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 10:50:05 +0900 Subject: [PATCH 18/29] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - if null 분기 제거 --- .../reviewme/global/DuplicateRequestInterceptor.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index 239e7de1a..163f79c2c 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -28,17 +28,13 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } String key = generateRequestKey(request); - Long frequency = redisTemplate.opsForValue().get(key); - if (frequency == null) { - redisTemplate.opsForValue().set(key, 1L, DURATION_SECOND); - return true; - } + redisTemplate.opsForValue().setIfAbsent(key, 0L, DURATION_SECOND); + redisTemplate.opsForValue().increment(key); + long frequency = redisTemplate.opsForValue().get(key); if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); } - - redisTemplate.opsForValue().increment(key); return true; } From 1212bbc5c7cc8efc1d83a87960a80a953b587675 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 10:50:47 +0900 Subject: [PATCH 19/29] =?UTF-8?q?test:=20=EA=B9=A8=EC=A7=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B4=89=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/reviewme/config/WebConfig.java | 2 +- .../global/DuplicateRequestInterceptorTest.java | 17 ++++++++--------- backend/src/test/resources/application.yml | 5 +++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java index 8057382b5..8df1e061c 100644 --- a/backend/src/main/java/reviewme/config/WebConfig.java +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -16,8 +16,8 @@ @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final RedisTemplate redisTemplate; private final ReviewGroupService reviewGroupService; - private final RedisTemplate redisTemplate; @Override public void addArgumentResolvers(List resolvers) { diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index 9d8f05ed9..fc1ea8258 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -6,22 +6,21 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.springframework.http.HttpHeaders.USER_AGENT; import jakarta.servlet.http.HttpServletRequest; import java.time.Duration; -import java.time.temporal.ChronoUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; -import reviewme.global.exception.RequestFrequencyNonNumericException; import reviewme.global.exception.TooManyDuplicateRequestException; class DuplicateRequestInterceptorTest { private final HttpServletRequest request = mock(HttpServletRequest.class); - private final RedisTemplate redisTemplate = mock(RedisTemplate.class); - private final ValueOperations valueOperations = mock(ValueOperations.class); + private final RedisTemplate redisTemplate = mock(RedisTemplate.class); + private final ValueOperations valueOperations = mock(ValueOperations.class); private final DuplicateRequestInterceptor interceptor = new DuplicateRequestInterceptor(redisTemplate); private final String requestKey = "RequestURI: /api/v2/reviews, RemoteAddr: localhost, UserAgent: Postman"; @@ -31,7 +30,7 @@ void setUp() { given(request.getRequestURI()).willReturn("/api/v2/reviews"); given(request.getRemoteAddr()).willReturn("localhost"); - given(request.getHeader("User-Agent")).willReturn("Postman"); + given(request.getHeader(USER_AGENT)).willReturn("Postman"); given(redisTemplate.opsForValue()).willReturn(valueOperations); } @@ -59,13 +58,13 @@ void setUp() { // then assertThat(result).isTrue(); - verify(valueOperations).set(requestKey, 1, Duration.of(1, ChronoUnit.SECONDS)); + verify(valueOperations).set(requestKey, 1L, Duration.ofSeconds(1)); } @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { // given - int frequency = 1; + long frequency = 1; given(valueOperations.get(anyString())).willReturn(frequency); // when @@ -73,13 +72,13 @@ void setUp() { // then assertThat(result).isTrue(); - verify(valueOperations).set(requestKey, frequency + 1, Duration.of(1, ChronoUnit.SECONDS)); + verify(valueOperations).increment(requestKey); } @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { // given - int maxFrequency = 3; + long maxFrequency = 3; given(valueOperations.get(anyString())).willReturn(maxFrequency + 1); // when & then diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 0c5a19c1e..d796c9291 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -38,3 +38,8 @@ logging: cors: allowed-origins: - https://allowed-domain.com + +# todo: 이게 맞는걸까..? +redis: + host: localhost + port: 6379 From 82cf806762b67a32cd42c7504f2f07b74fafb3ed Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 13:29:16 +0900 Subject: [PATCH 20/29] =?UTF-8?q?test:=20=EC=9D=98=EB=AF=B8=20=EC=97=86?= =?UTF-8?q?=EC=96=B4=EC=A7=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명시적으로 null 인 경우 1로 초기화했던 이전 코드와 달리, setIfAbsent를 통해서 값이 초기화하는 지금은 '1로 초기화되었는지' 검증하기가 어렵다. --- .../global/DuplicateRequestInterceptorTest.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index fc1ea8258..c0088b5af 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -9,7 +9,6 @@ import static org.springframework.http.HttpHeaders.USER_AGENT; import jakarta.servlet.http.HttpServletRequest; -import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; @@ -48,19 +47,6 @@ void setUp() { assertThat(result).isTrue(); } - @Test - void 특정_POST_요청이_처음인_경우_빈도를_1로_초기화한다() { - // given - given(valueOperations.get(anyString())).willReturn(null); - - // when - boolean result = interceptor.preHandle(request, null, null); - - // then - assertThat(result).isTrue(); - verify(valueOperations).set(requestKey, 1L, Duration.ofSeconds(1)); - } - @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { // given From 08ae9049dc15ab6c90e7b235719b23b61a0115d5 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 14:30:28 +0900 Subject: [PATCH 21/29] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A7=80=EC=97=AD=EB=B3=80=EC=88=98=20=ED=95=A0?= =?UTF-8?q?=EB=8B=B9=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - increment 가 증가한 결과를 바로 반환한다. 이를 사용하도록 수정했다. --- .../main/java/reviewme/global/DuplicateRequestInterceptor.java | 3 +-- .../java/reviewme/global/DuplicateRequestInterceptorTest.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index 163f79c2c..b487f88ff 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -29,9 +29,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String key = generateRequestKey(request); redisTemplate.opsForValue().setIfAbsent(key, 0L, DURATION_SECOND); - redisTemplate.opsForValue().increment(key); + long frequency = redisTemplate.opsForValue().increment(key); - long frequency = redisTemplate.opsForValue().get(key); if (frequency >= MAX_FREQUENCY) { throw new TooManyDuplicateRequestException(key); } diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index c0088b5af..68970472c 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -65,7 +65,7 @@ void setUp() { void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { // given long maxFrequency = 3; - given(valueOperations.get(anyString())).willReturn(maxFrequency + 1); + given(valueOperations.increment(anyString())).willReturn(maxFrequency + 1); // when & then assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) From 10eb99bce6a0f697122e2cae7d39cd970b2534bb Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 14:32:10 +0900 Subject: [PATCH 22/29] =?UTF-8?q?test:=20redisTemplate=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EA=B9=A8=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B4=89=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedisTemplate과 ValueOperations를 모킹한다. - 구체적인 인자를 지정하지 않은 stub 문을 수정한다. --- backend/src/test/java/reviewme/api/ApiTest.java | 17 +++++++++++++++++ .../test/java/reviewme/api/ReviewApiTest.java | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 72eb11e02..682a2ea18 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -1,5 +1,7 @@ package reviewme.api; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; @@ -15,8 +17,11 @@ import org.apache.http.HttpHeaders; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -73,6 +78,12 @@ public abstract class ApiTest { @MockBean protected ReviewGroupLookupService reviewGroupLookupService; + @MockBean + protected RedisTemplate redisTemplate; + + @Mock + protected ValueOperations valueOperations; + @MockBean protected ReviewSummaryService reviewSummaryService; @@ -100,6 +111,12 @@ public abstract class ApiTest { } }; + @BeforeEach + void setUpRedisConfig() { + given(redisTemplate.opsForValue()).willReturn(valueOperations); + given(valueOperations.increment(anyString())).willReturn(1L); + } + @BeforeEach void setUpRestDocs(WebApplicationContext context, RestDocumentationContextProvider provider) { UriModifyingOperationPreprocessor uriModifier = modifyUris() diff --git a/backend/src/test/java/reviewme/api/ReviewApiTest.java b/backend/src/test/java/reviewme/api/ReviewApiTest.java index 8df8fa70e..5add4cfbd 100644 --- a/backend/src/test/java/reviewme/api/ReviewApiTest.java +++ b/backend/src/test/java/reviewme/api/ReviewApiTest.java @@ -85,7 +85,7 @@ class ReviewApiTest extends ApiTest { @Test void 리뷰_그룹_코드가_올바르지_않은_경우_예외가_발생한다() { BDDMockito.given(reviewRegisterService.registerReview(any(ReviewRegisterRequest.class))) - .willThrow(new ReviewGroupNotFoundByReviewRequestCodeException(anyString())); + .willThrow(new ReviewGroupNotFoundByReviewRequestCodeException("ABCD1234")); FieldDescriptor[] requestFieldDescriptors = { fieldWithPath("reviewRequestCode").description("리뷰 요청 코드"), From 3d1e62329fe101e0aef8c0a66ef5bf00e930c16a Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 14:47:00 +0900 Subject: [PATCH 23/29] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reviewme/global/DuplicateRequestInterceptor.java | 4 ++-- .../java/reviewme/global/GlobalExceptionHandler.java | 6 +++--- .../exception/TooManyDuplicateRequestException.java | 12 ------------ .../global/exception/TooManyRequestException.java | 12 ++++++++++++ .../global/DuplicateRequestInterceptorTest.java | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java create mode 100644 backend/src/main/java/reviewme/global/exception/TooManyRequestException.java diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java index b487f88ff..8299257ab 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java @@ -10,7 +10,7 @@ import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import reviewme.global.exception.TooManyDuplicateRequestException; +import reviewme.global.exception.TooManyRequestException; @Component @RequiredArgsConstructor @@ -32,7 +32,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons long frequency = redisTemplate.opsForValue().increment(key); if (frequency >= MAX_FREQUENCY) { - throw new TooManyDuplicateRequestException(key); + throw new TooManyRequestException(key); } return true; } diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index 4e17d30b3..9d4511618 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -22,7 +22,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import reviewme.global.exception.BadRequestException; import reviewme.global.exception.DataInconsistencyException; -import reviewme.global.exception.TooManyDuplicateRequestException; +import reviewme.global.exception.TooManyRequestException; import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; @@ -51,8 +51,8 @@ public ProblemDetail handleDataConsistencyException(DataInconsistencyException e return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getErrorMessage()); } - @ExceptionHandler(TooManyDuplicateRequestException.class) - public ProblemDetail handleDuplicateRequestException(TooManyDuplicateRequestException ex) { + @ExceptionHandler(TooManyRequestException.class) + public ProblemDetail handleDuplicateRequestException(TooManyRequestException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS, ex.getErrorMessage()); } diff --git a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java deleted file mode 100644 index 4ebf1235b..000000000 --- a/backend/src/main/java/reviewme/global/exception/TooManyDuplicateRequestException.java +++ /dev/null @@ -1,12 +0,0 @@ -package reviewme.global.exception; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class TooManyDuplicateRequestException extends ReviewMeException { - - public TooManyDuplicateRequestException(String requestKey) { - super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); - log.warn("Too many duplicate request received: {}", requestKey); - } -} diff --git a/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java new file mode 100644 index 000000000..502f9c505 --- /dev/null +++ b/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java @@ -0,0 +1,12 @@ +package reviewme.global.exception; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TooManyRequestException extends ReviewMeException { + + public TooManyRequestException(String requestKey) { + super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); + log.warn("Too many request received - request: {}", requestKey); + } +} diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java index 68970472c..07e4667e2 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; -import reviewme.global.exception.TooManyDuplicateRequestException; +import reviewme.global.exception.TooManyRequestException; class DuplicateRequestInterceptorTest { @@ -69,6 +69,6 @@ void setUp() { // when & then assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) - .isInstanceOf(TooManyDuplicateRequestException.class); + .isInstanceOf(TooManyRequestException.class); } } From 87a4294d93ee3a87a3dee547ffc858f50442eda8 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 15:45:52 +0900 Subject: [PATCH 24/29] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EC=84=A4=EC=A0=95=EC=9D=84=20properties?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/reviewme/config/RedisProperties.java | 10 ---------- .../reviewme/config/RequestLimitProperties.java | 13 +++++++++++++ ...disConfig.java => RequestLimitRedisConfig.java} | 12 ++++++++---- .../src/main/java/reviewme/config/WebConfig.java | 7 ++++--- ...terceptor.java => RequestLimitInterceptor.java} | 14 +++++++------- backend/src/main/resources/application.yml | 4 +++- ...rTest.java => RequestLimitInterceptorTest.java} | 10 +++++++--- backend/src/test/resources/application.yml | 5 +++-- 8 files changed, 45 insertions(+), 30 deletions(-) delete mode 100644 backend/src/main/java/reviewme/config/RedisProperties.java create mode 100644 backend/src/main/java/reviewme/config/RequestLimitProperties.java rename backend/src/main/java/reviewme/config/{RedisConfig.java => RequestLimitRedisConfig.java} (65%) rename backend/src/main/java/reviewme/global/{DuplicateRequestInterceptor.java => RequestLimitInterceptor.java} (74%) rename backend/src/test/java/reviewme/global/{DuplicateRequestInterceptorTest.java => RequestLimitInterceptorTest.java} (84%) diff --git a/backend/src/main/java/reviewme/config/RedisProperties.java b/backend/src/main/java/reviewme/config/RedisProperties.java deleted file mode 100644 index 9e06e8715..000000000 --- a/backend/src/main/java/reviewme/config/RedisProperties.java +++ /dev/null @@ -1,10 +0,0 @@ -package reviewme.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "redis") -public record RedisProperties( - String host, - int port -) { -} diff --git a/backend/src/main/java/reviewme/config/RequestLimitProperties.java b/backend/src/main/java/reviewme/config/RequestLimitProperties.java new file mode 100644 index 000000000..cf0850a78 --- /dev/null +++ b/backend/src/main/java/reviewme/config/RequestLimitProperties.java @@ -0,0 +1,13 @@ +package reviewme.config; + +import java.time.Duration; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "request-limit") +public record RequestLimitProperties( + long maxFrequency, + Duration duration, + String host, + int port +) { +} diff --git a/backend/src/main/java/reviewme/config/RedisConfig.java b/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java similarity index 65% rename from backend/src/main/java/reviewme/config/RedisConfig.java rename to backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java index 42b4c4ed9..e47e5e66b 100644 --- a/backend/src/main/java/reviewme/config/RedisConfig.java +++ b/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java @@ -7,23 +7,27 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; @Configuration -@EnableConfigurationProperties(RedisProperties.class) +@EnableConfigurationProperties(RequestLimitProperties.class) @RequiredArgsConstructor -public class RedisConfig { +public class RequestLimitRedisConfig { - private final RedisProperties redisProperties; + private final RequestLimitProperties requestLimitProperties; @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisProperties.host(), redisProperties.port()); + return new LettuceConnectionFactory( + requestLimitProperties.host(), requestLimitProperties.port() + ); } @Bean public RedisTemplate requestFrequencyRedisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); return redisTemplate; } diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java index 8df1e061c..8ca25d2c7 100644 --- a/backend/src/main/java/reviewme/config/WebConfig.java +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -7,17 +7,18 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import reviewme.global.RequestLimitInterceptor; import reviewme.reviewgroup.controller.ReviewGroupSessionResolver; import reviewme.reviewgroup.service.ReviewGroupService; -import reviewme.global.DuplicateRequestInterceptor; import reviewme.global.HeaderPropertyArgumentResolver; @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { - private final RedisTemplate redisTemplate; private final ReviewGroupService reviewGroupService; + private final RedisTemplate redisTemplate; + private final RequestLimitProperties requestLimitProperties; @Override public void addArgumentResolvers(List resolvers) { @@ -26,6 +27,6 @@ public void addArgumentResolvers(List resolvers) @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new DuplicateRequestInterceptor(redisTemplate)); + registry.addInterceptor(new RequestLimitInterceptor(redisTemplate, requestLimitProperties)); } } diff --git a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java b/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java similarity index 74% rename from backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java rename to backend/src/main/java/reviewme/global/RequestLimitInterceptor.java index 8299257ab..9ecc511c1 100644 --- a/backend/src/main/java/reviewme/global/DuplicateRequestInterceptor.java +++ b/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java @@ -4,22 +4,22 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.time.Duration; import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import reviewme.config.RequestLimitProperties; import reviewme.global.exception.TooManyRequestException; @Component +@EnableConfigurationProperties(RequestLimitProperties.class) @RequiredArgsConstructor -public class DuplicateRequestInterceptor implements HandlerInterceptor { - - private static final int MAX_FREQUENCY = 3; - private static final Duration DURATION_SECOND = Duration.ofSeconds(1); +public class RequestLimitInterceptor implements HandlerInterceptor { private final RedisTemplate redisTemplate; + private final RequestLimitProperties requestLimitProperties; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { @@ -28,10 +28,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } String key = generateRequestKey(request); - redisTemplate.opsForValue().setIfAbsent(key, 0L, DURATION_SECOND); + redisTemplate.opsForValue().setIfAbsent(key, 0L, requestLimitProperties.duration()); long frequency = redisTemplate.opsForValue().increment(key); - if (frequency >= MAX_FREQUENCY) { + if (frequency >= requestLimitProperties.maxFrequency()) { throw new TooManyRequestException(key); } return true; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9c7461a74..d6d25c4d0 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -38,6 +38,8 @@ cors: - http://localhost - https://localhost -redis: +request-limit: + max-frequency: 3 + duration: 1s host: localhost port: 6379 diff --git a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java similarity index 84% rename from backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java rename to backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java index 07e4667e2..a26342d9a 100644 --- a/backend/src/test/java/reviewme/global/DuplicateRequestInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java @@ -9,29 +9,33 @@ import static org.springframework.http.HttpHeaders.USER_AGENT; import jakarta.servlet.http.HttpServletRequest; +import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; +import reviewme.config.RequestLimitProperties; import reviewme.global.exception.TooManyRequestException; -class DuplicateRequestInterceptorTest { +class RequestLimitInterceptorTest { private final HttpServletRequest request = mock(HttpServletRequest.class); private final RedisTemplate redisTemplate = mock(RedisTemplate.class); private final ValueOperations valueOperations = mock(ValueOperations.class); - private final DuplicateRequestInterceptor interceptor = new DuplicateRequestInterceptor(redisTemplate); + private final RequestLimitProperties requestLimitProperties = mock(RequestLimitProperties.class); + private final RequestLimitInterceptor interceptor = new RequestLimitInterceptor(redisTemplate, requestLimitProperties); private final String requestKey = "RequestURI: /api/v2/reviews, RemoteAddr: localhost, UserAgent: Postman"; @BeforeEach void setUp() { given(request.getMethod()).willReturn("POST"); - given(request.getRequestURI()).willReturn("/api/v2/reviews"); given(request.getRemoteAddr()).willReturn("localhost"); given(request.getHeader(USER_AGENT)).willReturn("Postman"); given(redisTemplate.opsForValue()).willReturn(valueOperations); + given(requestLimitProperties.duration()).willReturn(Duration.ofSeconds(1)); + given(requestLimitProperties.maxFrequency()).willReturn(3L); } @Test diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index d796c9291..bec2a0970 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -39,7 +39,8 @@ cors: allowed-origins: - https://allowed-domain.com -# todo: 이게 맞는걸까..? -redis: +request-limit: + max-frequency: 3 + duration: 1s host: localhost port: 6379 From 2c434ea570b7752c81d17d960404bf0999ded533 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 15:51:33 +0900 Subject: [PATCH 25/29] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테드 의견 반영 --- .../java/reviewme/global/exception/TooManyRequestException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java b/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java index 502f9c505..4f26fee3e 100644 --- a/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java +++ b/backend/src/main/java/reviewme/global/exception/TooManyRequestException.java @@ -6,7 +6,7 @@ public class TooManyRequestException extends ReviewMeException { public TooManyRequestException(String requestKey) { - super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요"); + super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요. 잠시 후 다시 시도해주세요."); log.warn("Too many request received - request: {}", requestKey); } } From 5c28d2e34a0c632fb04db34025361791c21dace3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 16:20:04 +0900 Subject: [PATCH 26/29] =?UTF-8?q?chore:=20=EC=95=88=EC=93=B0=EB=8A=94=20im?= =?UTF-8?q?port=20=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/reviewme/config/WebConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java index 8ca25d2c7..916ea5a41 100644 --- a/backend/src/main/java/reviewme/config/WebConfig.java +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -10,7 +10,6 @@ import reviewme.global.RequestLimitInterceptor; import reviewme.reviewgroup.controller.ReviewGroupSessionResolver; import reviewme.reviewgroup.service.ReviewGroupService; -import reviewme.global.HeaderPropertyArgumentResolver; @Configuration @RequiredArgsConstructor From ddd7c72cec8f35ae0b73e516dc10200e15b4855c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 16:20:20 +0900 Subject: [PATCH 27/29] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=BD=EA=B3=84=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/java/reviewme/global/RequestLimitInterceptorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java index a26342d9a..e8f59ab11 100644 --- a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java @@ -68,7 +68,7 @@ void setUp() { @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { // given - long maxFrequency = 3; + long maxFrequency = 2; given(valueOperations.increment(anyString())).willReturn(maxFrequency + 1); // when & then From cf7e6283158e9caa895bfdbf516bde781649ece3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 16:25:21 +0900 Subject: [PATCH 28/29] =?UTF-8?q?refactor:=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reviewme/config/RequestLimitProperties.java | 2 +- .../java/reviewme/global/RequestLimitInterceptor.java | 9 ++++++--- backend/src/main/resources/application.yml | 2 +- .../reviewme/global/RequestLimitInterceptorTest.java | 2 +- backend/src/test/resources/application.yml | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/reviewme/config/RequestLimitProperties.java b/backend/src/main/java/reviewme/config/RequestLimitProperties.java index cf0850a78..efea3b4f8 100644 --- a/backend/src/main/java/reviewme/config/RequestLimitProperties.java +++ b/backend/src/main/java/reviewme/config/RequestLimitProperties.java @@ -5,7 +5,7 @@ @ConfigurationProperties(prefix = "request-limit") public record RequestLimitProperties( - long maxFrequency, + long threshold, Duration duration, String host, int port diff --git a/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java b/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java index 9ecc511c1..b5747dfd1 100644 --- a/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java +++ b/backend/src/main/java/reviewme/global/RequestLimitInterceptor.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -28,10 +29,12 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } String key = generateRequestKey(request); - redisTemplate.opsForValue().setIfAbsent(key, 0L, requestLimitProperties.duration()); - long frequency = redisTemplate.opsForValue().increment(key); + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.setIfAbsent(key, 0L, requestLimitProperties.duration()); + redisTemplate.expire(key, requestLimitProperties.duration()); - if (frequency >= requestLimitProperties.maxFrequency()) { + long requestCount = valueOperations.increment(key); + if (requestCount > requestLimitProperties.threshold()) { throw new TooManyRequestException(key); } return true; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d6d25c4d0..aa0160b1f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -39,7 +39,7 @@ cors: - https://localhost request-limit: - max-frequency: 3 + threshold: 3 duration: 1s host: localhost port: 6379 diff --git a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java index e8f59ab11..1144989f3 100644 --- a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java @@ -35,7 +35,7 @@ void setUp() { given(redisTemplate.opsForValue()).willReturn(valueOperations); given(requestLimitProperties.duration()).willReturn(Duration.ofSeconds(1)); - given(requestLimitProperties.maxFrequency()).willReturn(3L); + given(requestLimitProperties.threshold()).willReturn(3L); } @Test diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index bec2a0970..f18542246 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -40,7 +40,7 @@ cors: - https://allowed-domain.com request-limit: - max-frequency: 3 + threshold: 3 duration: 1s host: localhost port: 6379 From f1a9d38e46612f365e765be4a93df7af649b967f Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Tue, 15 Oct 2024 16:30:10 +0900 Subject: [PATCH 29/29] =?UTF-8?q?refactor:=20frequency=20->=20requestCount?= =?UTF-8?q?=20=EC=99=84=EC=A0=84=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/reviewme/config/RequestLimitRedisConfig.java | 2 +- .../java/reviewme/global/RequestLimitInterceptorTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java b/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java index e47e5e66b..a8307db5f 100644 --- a/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java +++ b/backend/src/main/java/reviewme/config/RequestLimitRedisConfig.java @@ -24,7 +24,7 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - public RedisTemplate requestFrequencyRedisTemplate() { + public RedisTemplate requestLimitRedisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); diff --git a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java index 1144989f3..998639691 100644 --- a/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java +++ b/backend/src/test/java/reviewme/global/RequestLimitInterceptorTest.java @@ -54,8 +54,8 @@ void setUp() { @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { // given - long frequency = 1; - given(valueOperations.get(anyString())).willReturn(frequency); + long requestCount = 1; + given(valueOperations.get(anyString())).willReturn(requestCount); // when boolean result = interceptor.preHandle(request, null, null); @@ -68,8 +68,8 @@ void setUp() { @Test void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { // given - long maxFrequency = 2; - given(valueOperations.increment(anyString())).willReturn(maxFrequency + 1); + long maxRequestCount = 3; + given(valueOperations.increment(anyString())).willReturn(maxRequestCount + 1); // when & then assertThatThrownBy(() -> interceptor.preHandle(request, null, null))