Skip to content

Commit

Permalink
feat: query inspector 추가
Browse files Browse the repository at this point in the history
related issue #566
  • Loading branch information
jun02160 committed Dec 19, 2024
1 parent c5da3f0 commit 3899c57
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 0 deletions.
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 문제일 수 있습니다");
}
}
}
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();
}
}
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;
}
}
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) { }
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;
}
}
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 src/main/java/org/sopt/makers/internal/config/WebConfig.java
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("/**");
}
}
}

0 comments on commit 3899c57

Please sign in to comment.