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

[Feat] JPAQueryInspector 추가 #567

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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("/**");
}
}
}
Loading