-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
related issue #566
- Loading branch information
Showing
7 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
src/main/java/org/sopt/makers/internal/common/query/JPAQueryInspector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package org.sopt.makers.internal.common.query; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.hibernate.resource.jdbc.spi.StatementInspector; | ||
import org.springframework.stereotype.Component; | ||
|
||
import javax.validation.constraints.NotNull; | ||
import java.util.ArrayList; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class JPAQueryInspector implements StatementInspector { | ||
|
||
private static final ThreadLocal<QueryManager> queryManagers = new ThreadLocal<>(); | ||
|
||
void start() { | ||
queryManagers.set(new QueryManager( | ||
new ArrayList<>(), | ||
System.currentTimeMillis() | ||
)); | ||
} | ||
|
||
void finish() { | ||
queryManagers.remove(); | ||
} | ||
|
||
@Override | ||
public String inspect(String sql) { | ||
log.info("🚀sql: {}", sql); | ||
QueryManager queryManager = queryManagers.get(); | ||
if (queryManager != null) { | ||
queryManager.addQuery(sql); | ||
} | ||
return sql; | ||
} | ||
|
||
public QueryInspectResult inspectResult() { | ||
QueryManager queryManager = queryManagers.get(); | ||
long queryDurationTime = queryManager.calculateDuration(System.currentTimeMillis()); | ||
checkQueryCountIsOverThanMaxCount(queryManager); | ||
return new QueryInspectResult(queryManager.getQueryCount(), queryDurationTime); | ||
} | ||
|
||
private void checkQueryCountIsOverThanMaxCount(@NotNull QueryManager queryManager) { | ||
if (queryManager.isOverThanMaxQueryCount()) { | ||
log.warn("🚨쿼리가 10번 이상 실행되었습니다"); | ||
checkIsSusceptibleToNPlusOne(queryManager); | ||
} | ||
} | ||
|
||
private void checkIsSusceptibleToNPlusOne(@NotNull QueryManager queryManager) { | ||
NPlusOneDetector nPlusOneDetector = new NPlusOneDetector(queryManager.extractIndexOfSelectQuery()); | ||
if (nPlusOneDetector.isSelectCountOverThanWarnCount() && nPlusOneDetector.detect()) { | ||
log.warn("🚨select 문이 연속해서 5회 이상 실행되었습니다. N+1 문제일 수 있습니다"); | ||
} | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
src/main/java/org/sopt/makers/internal/common/query/JPAQueryManageInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package org.sopt.makers.internal.common.query; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
|
||
@Slf4j | ||
@RequiredArgsConstructor | ||
public class JPAQueryManageInterceptor implements HandlerInterceptor { | ||
|
||
private final JPAQueryInspector jpaQueryInspector; | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||
jpaQueryInspector.start(); | ||
return true; | ||
} | ||
|
||
@Override | ||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { | ||
QueryInspectResult queryInspectResult = jpaQueryInspector.inspectResult(); | ||
log.info("METHOD: [{}], URI: {}, QUERY_COUNT: {}, QUERY_EXECUTION_TIME: {} ms", | ||
request.getMethod(), | ||
request.getRequestURI(), | ||
queryInspectResult.count(), | ||
queryInspectResult.time() | ||
); | ||
jpaQueryInspector.finish(); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
src/main/java/org/sopt/makers/internal/common/query/NPlusOneDetector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package org.sopt.makers.internal.common.query; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
import java.util.List; | ||
|
||
@RequiredArgsConstructor | ||
public class NPlusOneDetector { | ||
|
||
private static final int N_PLUS_ONE_WARNING_COUNT = 5; | ||
private static final int SIGNAL_OF_IN_A_ROW = 1; | ||
|
||
private final List<Integer> values; | ||
|
||
public boolean detect() { | ||
for (int i=0; i<=values.size()-N_PLUS_ONE_WARNING_COUNT; i++) { | ||
if (isSequential(values.subList(i, i+N_PLUS_ONE_WARNING_COUNT))) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
private boolean isSequential(List<Integer> list) { | ||
for (int i=0; i<N_PLUS_ONE_WARNING_COUNT-1; i++) { | ||
if (list.get(i+1)-list.get(i) != SIGNAL_OF_IN_A_ROW) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
public boolean isSelectCountOverThanWarnCount() { | ||
return values.size() >= N_PLUS_ONE_WARNING_COUNT; | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
src/main/java/org/sopt/makers/internal/common/query/QueryInspectResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package org.sopt.makers.internal.common.query; | ||
|
||
|
||
public record QueryInspectResult(int count, long time) { } |
42 changes: 42 additions & 0 deletions
42
src/main/java/org/sopt/makers/internal/common/query/QueryManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package org.sopt.makers.internal.common.query; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
@RequiredArgsConstructor | ||
public class QueryManager { | ||
|
||
private static final String QUERY_TO_INSPECT_FOR_N_PLUS_ONE = "select"; | ||
private static final int MAX_QUERY_COUNT = 10; | ||
|
||
private final List<String> queries; | ||
private final long time; | ||
|
||
public List<Integer> extractIndexOfSelectQuery() { | ||
List<Integer> indexOfSelectQuery = new ArrayList<>(); | ||
for (int index = 0; index < queries.size(); index++) { | ||
if (queries.get(index).contains(QUERY_TO_INSPECT_FOR_N_PLUS_ONE)) { | ||
indexOfSelectQuery.add(index); | ||
} | ||
} | ||
return indexOfSelectQuery; | ||
} | ||
|
||
public void addQuery(String sql) { | ||
queries.add(sql); | ||
} | ||
|
||
public boolean isOverThanMaxQueryCount() { | ||
return getQueryCount() >= MAX_QUERY_COUNT; | ||
} | ||
|
||
public int getQueryCount() { | ||
return queries.size(); | ||
} | ||
|
||
public long calculateDuration(long afterQuery) { | ||
return afterQuery - time; | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/main/java/org/sopt/makers/internal/config/JPAQueryManageConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.sopt.makers.internal.config; | ||
|
||
import org.sopt.makers.internal.common.query.JPAQueryInspector; | ||
import org.sopt.makers.internal.common.query.JPAQueryManageInterceptor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Profile; | ||
|
||
@Configuration | ||
public class JPAQueryManageConfig { | ||
|
||
@Bean | ||
@Profile("local") | ||
public JPAQueryManageInterceptor jpaQueryManageInterceptor() { | ||
return new JPAQueryManageInterceptor(new JPAQueryInspector()); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/main/java/org/sopt/makers/internal/config/WebConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package org.sopt.makers.internal.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.sopt.makers.internal.common.query.JPAQueryManageInterceptor; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
private final JPAQueryManageInterceptor jpaQueryManageInterceptor; | ||
|
||
@Override | ||
public void addInterceptors(@NotNull InterceptorRegistry registry) { | ||
if (jpaQueryManageInterceptor != null) { | ||
registry.addInterceptor(jpaQueryManageInterceptor) | ||
.addPathPatterns("/**"); | ||
} | ||
} | ||
} |