Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#259 중복된 요청에 대한 에러 처리 #260

Merged
merged 50 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
80ccbd9
#259 [chore] redisson 의존성 추가
KWY0218 May 8, 2024
dd691be
#259 [feat] redis config 추가
KWY0218 May 8, 2024
4a10253
#259 [del] filter 내 component 제거
KWY0218 May 8, 2024
8052a7a
#259 [feat] duplicated interceptor 추가
sohyundoh May 8, 2024
6c88c73
#259 [feat] 예외 처리
sohyundoh May 8, 2024
894c086
#259 [fix] end point 수정
KWY0218 May 8, 2024
d810526
#259 [feat] request wrapping 방식 변경
KWY0218 May 8, 2024
3702ca3
#259 [feat] interceptor lock 방식 수정
KWY0218 May 8, 2024
96556e8
#259 [feat] too many request 예외처리
sohyundoh May 9, 2024
718889e
#259 [feat] 설정 파일 gitignore 추가
KWY0218 May 9, 2024
0ed1ec4
#259 [fix] profile 제거
KWY0218 May 9, 2024
032326d
#259 [fix] market place mysql 수정
KWY0218 May 9, 2024
1662203
#259 [fix] secrets 변수명 수정
KWY0218 May 9, 2024
ab63053
#259 [feat] build시 test 제거
KWY0218 May 9, 2024
1020e8d
#259 [feat] build 시 테스트 추가
KWY0218 May 9, 2024
5bbdf51
#259 [fix] yaml 수정
KWY0218 May 9, 2024
e20ec9f
259 [ci] edit db name
KWY0218 May 9, 2024
b284801
#259 [ci] check yml
KWY0218 May 9, 2024
f063385
#259 [ci] read secret
KWY0218 May 9, 2024
7154796
#259 [ci] read yaml
KWY0218 May 9, 2024
647f813
#259 [ci] read yaml
KWY0218 May 9, 2024
6508ad3
#259 [ci] edit mysql dependency
KWY0218 May 9, 2024
27d0d38
#259 [ci] dependency 수정
KWY0218 May 9, 2024
17c581c
#259 [ci] edit echo
KWY0218 May 9, 2024
5b946d6
#259 [ci] fix user name
KWY0218 May 9, 2024
92d45e8
#259 [ci] wait mysql
KWY0218 May 9, 2024
8a184a9
#259 [ci] wait mysql
KWY0218 May 9, 2024
4604ba2
#259 [ci] add root password
KWY0218 May 9, 2024
f1a797c
#259 [ci] cat mysql password
KWY0218 May 9, 2024
5d1e6b5
#259 [ci] change workflow
KWY0218 May 9, 2024
ac76386
#259 [ci] password
KWY0218 May 9, 2024
cc263f3
#259 [ci] user 정보 수정
KWY0218 May 9, 2024
1d64da4
#259 [ci] admin password
KWY0218 May 9, 2024
bb1a0bd
#259 [ci] add user
KWY0218 May 9, 2024
b76f3c3
#259 [ci] character set add
KWY0218 May 9, 2024
a96d1aa
#259 [ci] edit marketplace
KWY0218 May 9, 2024
918f562
#259 [ci] clean build
KWY0218 May 9, 2024
3c3f546
#259 [chore] springboot test 주석
KWY0218 May 9, 2024
e42aff5
#259 [ci] 주석 해제
KWY0218 May 9, 2024
3259fb1
#259 [ci] del transactional
KWY0218 May 9, 2024
efd665d
#259 [ci] springboot test 주석
KWY0218 May 9, 2024
10c7eae
#259 [feat] rmap key 생성 함수 추출
KWY0218 May 13, 2024
24f89d1
#259 [feat] meeting dto record로 이전
KWY0218 May 13, 2024
00ebdef
#259 [test] 중복 요청 테스트 코드 작성
KWY0218 May 13, 2024
750420c
#259 [feat] ci 내 redis 추가
KWY0218 May 13, 2024
d92323d
#259 [ci] edit ci mysql
KWY0218 May 13, 2024
5202300
#259 [ci] edit type
KWY0218 May 13, 2024
10ae8a0
#259 [ci] edit type
KWY0218 May 13, 2024
3c212b3
#259 [ci] change build
KWY0218 May 13, 2024
03d84e5
#259 [test] springboottest 주석 해제
KWY0218 May 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions .github/workflows/dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,34 @@ permissions:

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
redis-version: [6, 7]

steps:
- name: Setup MySQL
uses: samin/mysql-action@v1
uses: mirromutth/mysql-action@v1
with:
character set server: 'utf8'
mysql database: 'asap_dev'
mysql user: 'asap_dev_admin'
mysql password: ${{ secrets.DatabasePassword }}

mysql root password: 'asap'
mysql user: 'asap'
mysql password: 'asap'
host port: 3306
container port: 3306

- name: Set up redis
uses: supercharge/[email protected]
with:
node-version: ${{ matrix.node-version }}

- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: make application.properties 파일 생성
run: |
## create application.yml
Expand All @@ -51,10 +60,16 @@ jobs:
# application.yml 파일 확인
cat ./application.yml
shell: bash

# 이 워크플로우는 gradle build
- name: Grant execute permission for gradlew
run: chmod +x gradlew


- name: Wait for MySQL
run: |
while ! mysqladmin ping --host=127.0.0.1 --password='asap' --silent; do
sleep 1
done

- name: Build with Gradle # 실제 application build
run: ./gradlew build -PactiveProfiles=local
run: ./gradlew clean --stacktrace --info build
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ output/
*.log
*.log.gz
*.log-*.gz
logs/
logs/

/src/main/resources/application.yml
6 changes: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ repositories {
}

dependencies {
// QueryDSL Implementation
implementation "org.redisson:redisson:3.29.0"

// QueryDSL Implementation
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
Expand All @@ -41,7 +43,7 @@ dependencies {

// JPA & Database
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.32'
implementation 'mysql:mysql-connector-java:8.0.33'

// SWAGGER
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
import com.asap.server.exception.model.ConflictException;
import com.asap.server.exception.model.ForbiddenException;
import com.asap.server.exception.model.HostTimeForbiddenException;
import com.asap.server.exception.model.InternalErrorException;
import com.asap.server.exception.model.NotFoundException;
import com.asap.server.exception.model.TooManyRequestException;
import com.asap.server.exception.model.UnauthorizedException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
Expand All @@ -24,9 +29,6 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import java.io.IOException;

import static com.asap.server.exception.Error.METHOD_NOT_ALLOWED_EXCEPTION;
Expand Down Expand Up @@ -133,9 +135,25 @@ protected ErrorResponse handleConflictException(final ConflictException e) {
return ErrorResponse.error(e.getError());
}

/*
* 429 Too Many Requests
*/
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
@ExceptionHandler(TooManyRequestException.class)
protected ErrorResponse handleTooManyConflictException(final TooManyRequestException e) {
return ErrorResponse.error(e.getError());
}

/**
* 500 Internal Server
*/

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(InternalErrorException.class)
protected ErrorResponse handleInternalErrorException(final InternalErrorException e) {
return ErrorResponse.error(e.getError());
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ErrorResponse handleException(final Exception error, final HttpServletRequest request) throws IOException {
Expand Down
13 changes: 4 additions & 9 deletions src/main/java/com/asap/server/common/aspect/LoggingAspect.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.asap.server.common.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.asap.server.common.filter.CustomHttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
Expand All @@ -9,9 +10,7 @@
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.ContentCachingRequestWrapper;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -21,8 +20,6 @@
@Slf4j
public class LoggingAspect {

private final ObjectMapper objectMapper = new ObjectMapper();

@Pointcut("execution(* com.asap.server.controller..*(..)) || ( execution(* com.asap.server.common.advice..*(..)) && !execution(* com.asap.server.common.advice.ControllerExceptionAdvice.handleException*(..)))")
public void controllerInfoLevelExecute() {
}
Expand All @@ -34,15 +31,14 @@ public void controllerErrorLevelExecute() {
@Around("com.asap.server.common.aspect.LoggingAspect.controllerInfoLevelExecute()")
public Object requestInfoLevelLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
long startAt = System.currentTimeMillis();
Object returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
long endAt = System.currentTimeMillis();

log.info("================================================NEW===============================================");
log.info("====> Request: {} {} ({}ms)\n *Header = {}", request.getMethod(), request.getRequestURL(), endAt - startAt, getHeaders(request));
if ("POST".equalsIgnoreCase(request.getMethod())) {
log.info("====> Body: {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()));
log.info("====> Body: {}", ((CustomHttpServletRequestWrapper) request).getBody());
}
if (returnValue != null) {
log.info("====> Response: {}", returnValue);
Expand All @@ -54,14 +50,13 @@ public Object requestInfoLevelLogging(ProceedingJoinPoint proceedingJoinPoint) t
@Around("com.asap.server.common.aspect.LoggingAspect.controllerErrorLevelExecute()")
public Object requestErrorLevelLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
long startAt = System.currentTimeMillis();
Object returnValue = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
long endAt = System.currentTimeMillis();

log.error("====> Request: {} {} ({}ms)\n *Header = {}", request.getMethod(), request.getRequestURL(), endAt - startAt, getHeaders(request));
if ("POST".equalsIgnoreCase(request.getMethod())) {
log.error("====> Body: {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()));
log.error("====> Body: {}", ((CustomHttpServletRequestWrapper) request).getBody());
}
if (returnValue != null) {
log.error("====> Response: {}", returnValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.asap.server.common.filter;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.Getter;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

@Getter
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String body;

public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = readBody(request);
}

@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {

}

@Override
public int read() throws IOException {
return byteInputStream.read();
}
};
}

private String readBody(HttpServletRequest request) throws IOException {
BufferedReader input = new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();

var line = input.readLine();
while (line != null) {
sb.append(line.trim());
line = input.readLine();
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
package com.asap.server.common.filter;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomServletWrappingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
import java.io.IOException;

chain.doFilter(requestWrapper, responseWrapper);
public class CustomServletWrappingFilter implements Filter {

responseWrapper.copyBodyToResponse();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = new CustomHttpServletRequestWrapper((HttpServletRequest) servletRequest);
filterChain.doFilter(request, servletResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.asap.server.common.interceptor;


import com.asap.server.common.filter.CustomHttpServletRequestWrapper;
import com.asap.server.exception.Error;
import com.asap.server.exception.model.TooManyRequestException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
@RequiredArgsConstructor
public class DuplicatedInterceptor implements HandlerInterceptor {
private static final String REDIS_KEY = "ASAP_REDIS";
private static final String RMAP_VALUE = "ASAP";
private static final String RMAP_KEY_FORMAT = "LOCK [ ip : %s , body : %s ]";
private static final String USER_IP_HEADER = "x-real-ip";
private final RedissonClient redissonClient;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (lock(request)) return true;
throw new TooManyRequestException(Error.TOO_MANY_REQUEST_EXCEPTION);
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
unLock(request);
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
unLock(request);
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}

private String getRmapKey(HttpServletRequest request) {
final String body = ((CustomHttpServletRequestWrapper) request).getBody();
final String userIp = request.getHeader(USER_IP_HEADER);
return String.format(RMAP_KEY_FORMAT, userIp, body);
}

private boolean lock(HttpServletRequest request) {
final String rmapKey = getRmapKey(request);
RMap<String, String> redissonClientMap = redissonClient.getMap(REDIS_KEY);
return redissonClientMap.putIfAbsent(rmapKey, RMAP_VALUE) == null;
}

private void unLock(HttpServletRequest request) {
final String rmapKey = getRmapKey(request);
RMap<String, String> redissonClientMap = redissonClient.getMap(REDIS_KEY);
redissonClientMap.remove(rmapKey);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/asap/server/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.asap.server.config;

import com.asap.server.common.interceptor.DuplicatedInterceptor;
import com.asap.server.config.resolver.meeting.MeetingPathVariableResolver;
import com.asap.server.config.resolver.user.UserIdResolver;
import lombok.RequiredArgsConstructor;
Expand All @@ -9,6 +10,7 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;
Expand All @@ -18,6 +20,7 @@
public class WebConfig implements WebMvcConfigurer {
private final UserIdResolver userIdResolver;
private final MeetingPathVariableResolver meetingPathVariableResolver;
private final DuplicatedInterceptor duplicatedInterceptor;

@Bean
public PasswordEncoder getPasswordEncoder() {
Expand All @@ -37,4 +40,10 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
resolvers.add(userIdResolver);
resolvers.add(meetingPathVariableResolver);
}

@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
interceptorRegistry.addInterceptor(duplicatedInterceptor)
.addPathPatterns("/meeting", "/user/{meetingId}/time", "/user/host/{meetingId}/time");
}
}
Loading
Loading