diff --git a/src/main/java/org/sopt/makers/internal/common/query/JPAQueryInspector.java b/src/main/java/org/sopt/makers/internal/common/query/JPAQueryInspector.java new file mode 100644 index 00000000..05b6fa69 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/common/query/JPAQueryInspector.java @@ -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 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 문제일 수 μžˆμŠ΅λ‹ˆλ‹€"); + } + } +} diff --git a/src/main/java/org/sopt/makers/internal/common/query/JPAQueryManageInterceptor.java b/src/main/java/org/sopt/makers/internal/common/query/JPAQueryManageInterceptor.java new file mode 100644 index 00000000..bbc09542 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/common/query/JPAQueryManageInterceptor.java @@ -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(); + } +} diff --git a/src/main/java/org/sopt/makers/internal/common/query/NPlusOneDetector.java b/src/main/java/org/sopt/makers/internal/common/query/NPlusOneDetector.java new file mode 100644 index 00000000..498e4461 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/common/query/NPlusOneDetector.java @@ -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 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 list) { + for (int i=0; i= N_PLUS_ONE_WARNING_COUNT; + } +} diff --git a/src/main/java/org/sopt/makers/internal/common/query/QueryInspectResult.java b/src/main/java/org/sopt/makers/internal/common/query/QueryInspectResult.java new file mode 100644 index 00000000..ca9fd92f --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/common/query/QueryInspectResult.java @@ -0,0 +1,4 @@ +package org.sopt.makers.internal.common.query; + + +public record QueryInspectResult(int count, long time) { } diff --git a/src/main/java/org/sopt/makers/internal/common/query/QueryManager.java b/src/main/java/org/sopt/makers/internal/common/query/QueryManager.java new file mode 100644 index 00000000..5997c13c --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/common/query/QueryManager.java @@ -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 queries; + private final long time; + + public List extractIndexOfSelectQuery() { + List 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; + } +} diff --git a/src/main/java/org/sopt/makers/internal/config/JPAQueryManageConfig.java b/src/main/java/org/sopt/makers/internal/config/JPAQueryManageConfig.java new file mode 100644 index 00000000..12725444 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/config/JPAQueryManageConfig.java @@ -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()); + } +} diff --git a/src/main/java/org/sopt/makers/internal/config/WebConfig.java b/src/main/java/org/sopt/makers/internal/config/WebConfig.java new file mode 100644 index 00000000..e44773c4 --- /dev/null +++ b/src/main/java/org/sopt/makers/internal/config/WebConfig.java @@ -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("/**"); + } + } +}