From 6f81c2698c43191ac5298520711177a7e2d9c707 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 03:13:14 +0900 Subject: [PATCH 01/14] =?UTF-8?q?chore=20:=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=EC=97=90=EC=84=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=A0=88=EB=B2=A8=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=86=8C=EB=AC=B8=EC=9E=90=EB=A1=9C=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/jeyong/detector/config/NPlusOneDetectorConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detector/src/main/java/io/jeyong/detector/config/NPlusOneDetectorConfig.java b/detector/src/main/java/io/jeyong/detector/config/NPlusOneDetectorConfig.java index 47e9f2e..d5608c1 100644 --- a/detector/src/main/java/io/jeyong/detector/config/NPlusOneDetectorConfig.java +++ b/detector/src/main/java/io/jeyong/detector/config/NPlusOneDetectorConfig.java @@ -36,7 +36,7 @@ public NPlusOneDetectorConfig(final NPlusOneDetectorProperties nPlusOneDetectorP public void logInitialization() { logger.info("N+1 Detector is enabled with threshold: {}, log level: {}", nPlusOneDetectorProperties.getThreshold(), - nPlusOneDetectorProperties.getLevel().toString()); + nPlusOneDetectorProperties.getLevel().toString().toLowerCase()); } @Bean From f8bd4dd4f7836ea82f2872f31ba961d0f4a64aee Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 05:34:23 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat=20:=20EntityManagerFactory=EC=9D=98?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A1=9C=20N+1=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=8D=98?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EA=B2=83=EC=9D=B4=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detector/context/ExceptionContext.java | 20 ++++++++++++++++ .../context/ExceptionContextHolder.java | 24 +++++++++++++++++++ .../NPlusOneQueryExceptionCollector.java | 16 +++++++++++++ .../NPlusOneQueryExceptionThrower.java | 15 ------------ 4 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java create mode 100644 detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java create mode 100644 detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java delete mode 100644 detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionThrower.java diff --git a/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java b/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java new file mode 100644 index 0000000..5634b18 --- /dev/null +++ b/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java @@ -0,0 +1,20 @@ +package io.jeyong.detector.context; + +import io.jeyong.detector.test.NPlusOneQueryException; + +public final class ExceptionContext { + + private NPlusOneQueryException primaryException; + + void addSuppressPrimaryException(final NPlusOneQueryException exception) { + if (primaryException != null) { + primaryException.addSuppressed(exception); + } else { + primaryException = exception; + } + } + + public NPlusOneQueryException getPrimaryException() { + return primaryException; + } +} diff --git a/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java b/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java new file mode 100644 index 0000000..b01afe0 --- /dev/null +++ b/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java @@ -0,0 +1,24 @@ +package io.jeyong.detector.context; + +import io.jeyong.detector.test.NPlusOneQueryException; + +public final class ExceptionContextHolder { + + private static final ThreadLocal exceptionContextThreadLocal = + ThreadLocal.withInitial(ExceptionContext::new); + + private ExceptionContextHolder() { + } + + public static void saveException(final NPlusOneQueryException exception) { + exceptionContextThreadLocal.get().addSuppressPrimaryException(exception); + } + + public static ExceptionContext getContext() { + return exceptionContextThreadLocal.get(); + } + + public static void clearContext() { + exceptionContextThreadLocal.remove(); + } +} diff --git a/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java b/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java new file mode 100644 index 0000000..e3e29e5 --- /dev/null +++ b/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java @@ -0,0 +1,16 @@ +package io.jeyong.detector.template; + +import io.jeyong.detector.context.ExceptionContextHolder; +import io.jeyong.detector.test.NPlusOneQueryException; + +public final class NPlusOneQueryExceptionCollector extends NPlusOneQueryTemplate { + + public NPlusOneQueryExceptionCollector(final int queryThreshold) { + super(queryThreshold); + } + + @Override + protected void handleDetectedNPlusOneIssue(final String query, final Long count) { + ExceptionContextHolder.saveException(new NPlusOneQueryException(query, count)); + } +} diff --git a/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionThrower.java b/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionThrower.java deleted file mode 100644 index 2225d17..0000000 --- a/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionThrower.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.jeyong.detector.template; - -import io.jeyong.detector.test.NPlusOneQueryException; - -public final class NPlusOneQueryExceptionThrower extends NPlusOneQueryTemplate { - - public NPlusOneQueryExceptionThrower(final int queryThreshold) { - super(queryThreshold); - } - - @Override - protected void handleDetectedNPlusOneIssue(final String query, final Long count) { - throw new NPlusOneQueryException(query, count); - } -} From fa92b051c41bc3c5b8c59dd04320a26c5370ee58 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 05:35:02 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=9D=98=20afterEach=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=9C=20=EC=98=88=EC=99=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/DetectNPlusOneExtension.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java index aaa4877..f761c81 100644 --- a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java +++ b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java @@ -1,31 +1,28 @@ package io.jeyong.detector.test; +import io.jeyong.detector.context.ExceptionContextHolder; import io.jeyong.detector.context.QueryContextHolder; -import io.jeyong.detector.template.NPlusOneQueryExceptionThrower; -import io.jeyong.detector.template.NPlusOneQueryTemplate; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; public class DetectNPlusOneExtension implements BeforeEachCallback, AfterEachCallback { - private final NPlusOneQueryTemplate nPlusOneQueryTemplate; - - public DetectNPlusOneExtension() { - this(2); - } - - public DetectNPlusOneExtension(int queryThreshold) { - this.nPlusOneQueryTemplate = new NPlusOneQueryExceptionThrower(queryThreshold); - } - @Override public void beforeEach(ExtensionContext context) throws Exception { QueryContextHolder.clearContext(); + ExceptionContextHolder.clearContext(); } @Override - public void afterEach(ExtensionContext context) throws Exception { - nPlusOneQueryTemplate.handleNPlusOneIssues(); + public void afterEach(final ExtensionContext context) throws Exception { + try { + final NPlusOneQueryException primaryException = ExceptionContextHolder.getContext().getPrimaryException(); + if (primaryException != null) { + throw primaryException; + } + } finally { + ExceptionContextHolder.clearContext(); + } } } From db53472270f7c0a503fad7cdad32a9a4822c8568 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 05:38:17 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat=20:=20@Primary=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=A1=9C=EA=B7=B8=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=EC=98=88=EC=99=B8=EB=A5=BC=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=EC=8B=9C=ED=82=AC=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detector/test/NPlusOneTestConfig.java | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java index f78bbcb..0b86ff4 100644 --- a/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java +++ b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java @@ -1,23 +1,39 @@ package io.jeyong.detector.test; -import static org.hibernate.cfg.AvailableSettings.STATEMENT_INSPECTOR; - -import io.jeyong.detector.interceptor.QueryStatementInspector; -import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import io.jeyong.detector.config.NPlusOneDetectorConfig; +import io.jeyong.detector.config.NPlusOneDetectorProperties; +import io.jeyong.detector.template.NPlusOneQueryExceptionCollector; +import io.jeyong.detector.template.NPlusOneQueryTemplate; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; @Configuration +@Import(NPlusOneDetectorConfig.class) +@EnableConfigurationProperties(NPlusOneDetectorProperties.class) public class NPlusOneTestConfig { - @Bean - public QueryStatementInspector queryStatementInspector() { - return new QueryStatementInspector(); + private static final Logger logger = LoggerFactory.getLogger(NPlusOneTestConfig.class); + private final NPlusOneDetectorProperties nPlusOneDetectorProperties; + + public NPlusOneTestConfig(final NPlusOneDetectorProperties nPlusOneDetectorProperties) { + this.nPlusOneDetectorProperties = nPlusOneDetectorProperties; + } + + @PostConstruct + public void logInitialization() { + logger.info("N+1 issues detected will throw exceptions."); } + @Primary @Bean - public HibernatePropertiesCustomizer hibernatePropertiesCustomizer( - final QueryStatementInspector queryStatementInspector) { - return hibernateProperties -> hibernateProperties.put(STATEMENT_INSPECTOR, queryStatementInspector); + public NPlusOneQueryTemplate nPlusOneQueryExceptionCollector() { + return new NPlusOneQueryExceptionCollector( + nPlusOneDetectorProperties.getThreshold()); } } From 05afb50bab14e12b7e72c17f45fd33dd4411a3bb Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 05:39:15 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EB=A1=9C=EA=B7=B8=20=EB=98=90=EB=8A=94=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EB=B0=9C=EC=83=9D=EC=9D=84=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/jeyong/detector/test/NPlusOneTest.java | 27 ++++++++++++++++ .../test/NplusOneTestImportSelector.java | 31 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 detector/src/main/java/io/jeyong/detector/test/NPlusOneTest.java create mode 100644 detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java diff --git a/detector/src/main/java/io/jeyong/detector/test/NPlusOneTest.java b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTest.java new file mode 100644 index 0000000..0f12836 --- /dev/null +++ b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTest.java @@ -0,0 +1,27 @@ +package io.jeyong.detector.test; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.event.Level; +import org.springframework.context.annotation.Import; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DetectNPlusOneExtension.class) +@Import(NplusOneTestImportSelector.class) +public @interface NPlusOneTest { + + int threshold() default 2; + + Level level() default Level.WARN; + + Mode mode() default Mode.LOGGING; + + enum Mode { + LOGGING, + EXCEPTION + } +} diff --git a/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java b/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java new file mode 100644 index 0000000..1eed87b --- /dev/null +++ b/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java @@ -0,0 +1,31 @@ +package io.jeyong.detector.test; + +import io.jeyong.detector.config.NPlusOneDetectorConfig; +import java.util.Map; +import org.slf4j.event.Level; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.lang.NonNull; + +public class NplusOneTestImportSelector implements ImportSelector { + + @NonNull + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + Map attributes = importingClassMetadata + .getAnnotationAttributes(NPlusOneTest.class.getName()); + + assert attributes != null; + int threshold = (int) attributes.get("threshold"); + Level logLevel = (Level) attributes.get("level"); + NPlusOneTest.Mode mode = (NPlusOneTest.Mode) attributes.get("mode"); + + System.setProperty("spring.jpa.properties.hibernate.detector.threshold", String.valueOf(threshold)); + System.setProperty("spring.jpa.properties.hibernate.detector.level", logLevel.toString()); + return new String[]{ + mode == NPlusOneTest.Mode.LOGGING + ? NPlusOneDetectorConfig.class.getName() + : NPlusOneTestConfig.class.getName() + }; + } +} From ec402e6107da2e7ebdc141aa10a4f92e2bb20d96 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 06:06:40 +0900 Subject: [PATCH 06/14] =?UTF-8?q?fix=20:=20enabled=20=EA=B0=92=EC=9D=B4=20?= =?UTF-8?q?false=20=EC=9D=B4=EB=A9=B4=20@NPlusOneTest=EA=B0=80=20=EC=9E=91?= =?UTF-8?q?=EB=8F=99=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/jeyong/detector/test/NplusOneTestImportSelector.java | 1 + test/src/main/resources/application.yml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java b/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java index 1eed87b..b505fb2 100644 --- a/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java +++ b/detector/src/main/java/io/jeyong/detector/test/NplusOneTestImportSelector.java @@ -20,6 +20,7 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { Level logLevel = (Level) attributes.get("level"); NPlusOneTest.Mode mode = (NPlusOneTest.Mode) attributes.get("mode"); + System.setProperty("spring.jpa.properties.hibernate.detector.enabled", "true"); System.setProperty("spring.jpa.properties.hibernate.detector.threshold", String.valueOf(threshold)); System.setProperty("spring.jpa.properties.hibernate.detector.level", logLevel.toString()); return new String[]{ diff --git a/test/src/main/resources/application.yml b/test/src/main/resources/application.yml index bd8c2fd..bff63f4 100644 --- a/test/src/main/resources/application.yml +++ b/test/src/main/resources/application.yml @@ -17,8 +17,8 @@ spring: highlight_sql: true hbm2ddl.auto: create-drop detector: - enabled: true # N+1 문제 감지 기능 활성화 여부 / 기본 값 : false - threshold: 2 # 쿼리 실행 횟수 기준 설정 (예: 3회 이상 실행된 쿼리에 대해 N+1 문제 감지) / 기본 값 : 2 - level: warn # 감지된 N+1 문제에 대한 로깅 레벨 설정 (error, warn, info, debug) / 기본 값 : warn + enabled: true # N+1 문제 감지 기능 활성화 여부 / 기본 값 : false + threshold: 2 # 쿼리 실행 횟수 기준 설정 (예: 3회 이상 실행된 쿼리에 대해 N+1 문제 감지) / 기본 값 : 2 + level: warn # 감지된 N+1 문제에 대한 로깅 레벨 설정 (error, warn, info, debug, trace) / 기본 값 : warn # open-in-view: false show-sql: true From 2501b18a4d6e743890d6a8dd2cf2a6e7ccee8b91 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 06:07:44 +0900 Subject: [PATCH 07/14] =?UTF-8?q?test=20:=20@NPlusOneTest=EC=9D=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8A=94=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=98=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/exception/ExceptionThrowerTest.java | 93 ------------------- .../mode/AnnotationExceptionModeTest.java | 27 ++++++ .../test/mode/AnnotationLoggingModeTest.java | 28 ++++++ 3 files changed, 55 insertions(+), 93 deletions(-) delete mode 100644 test/src/test/java/io/jeyong/test/exception/ExceptionThrowerTest.java create mode 100644 test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java create mode 100644 test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java diff --git a/test/src/test/java/io/jeyong/test/exception/ExceptionThrowerTest.java b/test/src/test/java/io/jeyong/test/exception/ExceptionThrowerTest.java deleted file mode 100644 index 62e149d..0000000 --- a/test/src/test/java/io/jeyong/test/exception/ExceptionThrowerTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package io.jeyong.test.exception; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import io.jeyong.detector.template.NPlusOneQueryExceptionThrower; -import io.jeyong.detector.template.NPlusOneQueryTemplate; -import io.jeyong.detector.test.NPlusOneQueryException; -import io.jeyong.detector.test.NPlusOneTestConfig; -import io.jeyong.test.case2.entity.Order; -import io.jeyong.test.case2.entity.Product; -import io.jeyong.test.case2.repository.OrderRepository; -import io.jeyong.test.case2.repository.ProductRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; - -@Import(NPlusOneTestConfig.class) -@DataJpaTest -class ExceptionThrowerTest { - - @Autowired - private OrderRepository orderRepository; - - @Autowired - private ProductRepository productRepository; - - @PersistenceContext - private EntityManager em; - - private final NPlusOneQueryTemplate nPlusOneQueryTemplate = new NPlusOneQueryExceptionThrower(2); - - @Test - @DisplayName("테스트 코드에서는 예외가 발생한다.") - void testExceptionThrown() { - // given - setupTestData(); - clearPersistenceContext(); - // when - List products = productRepository.findAll(); - products.forEach(product -> product.getOrder().getOrderNumber()); - // then - assertThatThrownBy(nPlusOneQueryTemplate::handleNPlusOneIssues) - .isInstanceOf(NPlusOneQueryException.class) - .hasMessageContaining("N+1 issue detected"); - } - - private void clearPersistenceContext() { - em.flush(); - em.clear(); - } - - private void setupTestData() { - Order order1 = new Order(); - order1.setOrderNumber("Order 1"); - - Order order2 = new Order(); - order2.setOrderNumber("Order 2"); - - Order order3 = new Order(); - order3.setOrderNumber("Order 3"); - - orderRepository.save(order1); - orderRepository.save(order2); - orderRepository.save(order3); - - Product product1 = new Product(); - product1.setName("Product 1"); - product1.setPrice(100.0); - product1.setOrder(order1); - - Product product2 = new Product(); - product2.setName("Product 2"); - product2.setPrice(200.0); - product2.setOrder(order1); - - Product product3 = new Product(); - product3.setName("Product 3"); - product3.setPrice(300.0); - product3.setOrder(order2); - - Product product4 = new Product(); - product4.setName("Product 4"); - product4.setPrice(150.0); - product4.setOrder(order3); - - productRepository.saveAll(List.of(product1, product2, product3, product4)); - } -} diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java new file mode 100644 index 0000000..c76c066 --- /dev/null +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java @@ -0,0 +1,27 @@ +package io.jeyong.test.mode; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.jeyong.detector.test.NPlusOneTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest +@NPlusOneTest(threshold = 5, mode = NPlusOneTest.Mode.EXCEPTION) +class AnnotationExceptionModeTest { + + @Autowired + private ApplicationContext context; + + @Test + @DisplayName("EXCEPTION 모드의 설정이 제대로 동작한다.") + void testNPlusOneTestExceptionMode() { + assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("5"); + assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.level")).isEqualTo("WARN"); + + assertThat(context.containsBean("nPlusOneQueryExceptionCollector")).isTrue(); + } +} diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java new file mode 100644 index 0000000..a051c8b --- /dev/null +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java @@ -0,0 +1,28 @@ +package io.jeyong.test.mode; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import io.jeyong.detector.test.NPlusOneTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest +@NPlusOneTest(threshold = 3, level = Level.DEBUG, mode = NPlusOneTest.Mode.LOGGING) +class AnnotationLoggingModeTest { + + @Autowired + private ApplicationContext context; + + @Test + @DisplayName("LOGGING 모드의 설정이 제대로 동작한다.") + void testNPlusOneTestLoggingMode() { + assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("3"); + assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.level")).isEqualTo("DEBUG"); + + assertThat(context.containsBean("nPlusOneQueryLogger")).isTrue(); + } +} From 2e58af60d0c5f000a24687f8dbaf3f5c8cdf14af Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:11:18 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor=20:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=EA=B8=B0=EB=8A=A5=EC=9D=B4=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=EC=9D=98=20=EC=8A=A4=EB=A0=88=EB=93=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A1=9C=20ThreadLocal=EB=A5=BC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detector/context/ExceptionContext.java | 10 ++-- .../context/ExceptionContextHolder.java | 24 --------- .../NPlusOneQueryExceptionCollector.java | 9 ++-- .../test/DetectNPlusOneExtension.java | 50 ++++++++++++++----- .../detector/test/NPlusOneTestConfig.java | 10 +++- .../mode/AnnotationExceptionModeTest.java | 2 +- 6 files changed, 60 insertions(+), 45 deletions(-) delete mode 100644 detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java diff --git a/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java b/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java index 5634b18..1d10cf7 100644 --- a/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java +++ b/detector/src/main/java/io/jeyong/detector/context/ExceptionContext.java @@ -2,11 +2,11 @@ import io.jeyong.detector.test.NPlusOneQueryException; -public final class ExceptionContext { +public final class ExceptionContext { // Todo: Consider concurrency issues private NPlusOneQueryException primaryException; - void addSuppressPrimaryException(final NPlusOneQueryException exception) { + public void saveException(final NPlusOneQueryException exception) { if (primaryException != null) { primaryException.addSuppressed(exception); } else { @@ -14,7 +14,11 @@ void addSuppressPrimaryException(final NPlusOneQueryException exception) { } } - public NPlusOneQueryException getPrimaryException() { + public NPlusOneQueryException getException() { return primaryException; } + + public void clearException() { + primaryException = null; + } } diff --git a/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java b/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java deleted file mode 100644 index b01afe0..0000000 --- a/detector/src/main/java/io/jeyong/detector/context/ExceptionContextHolder.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.jeyong.detector.context; - -import io.jeyong.detector.test.NPlusOneQueryException; - -public final class ExceptionContextHolder { - - private static final ThreadLocal exceptionContextThreadLocal = - ThreadLocal.withInitial(ExceptionContext::new); - - private ExceptionContextHolder() { - } - - public static void saveException(final NPlusOneQueryException exception) { - exceptionContextThreadLocal.get().addSuppressPrimaryException(exception); - } - - public static ExceptionContext getContext() { - return exceptionContextThreadLocal.get(); - } - - public static void clearContext() { - exceptionContextThreadLocal.remove(); - } -} diff --git a/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java b/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java index e3e29e5..84e4aa8 100644 --- a/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java +++ b/detector/src/main/java/io/jeyong/detector/template/NPlusOneQueryExceptionCollector.java @@ -1,16 +1,19 @@ package io.jeyong.detector.template; -import io.jeyong.detector.context.ExceptionContextHolder; +import io.jeyong.detector.context.ExceptionContext; import io.jeyong.detector.test.NPlusOneQueryException; public final class NPlusOneQueryExceptionCollector extends NPlusOneQueryTemplate { - public NPlusOneQueryExceptionCollector(final int queryThreshold) { + private final ExceptionContext exceptionContext; + + public NPlusOneQueryExceptionCollector(final int queryThreshold, final ExceptionContext exceptionContext) { super(queryThreshold); + this.exceptionContext = exceptionContext; } @Override protected void handleDetectedNPlusOneIssue(final String query, final Long count) { - ExceptionContextHolder.saveException(new NPlusOneQueryException(query, count)); + exceptionContext.saveException(new NPlusOneQueryException(query, count)); } } diff --git a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java index f761c81..83682d2 100644 --- a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java +++ b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java @@ -1,28 +1,54 @@ package io.jeyong.detector.test; -import io.jeyong.detector.context.ExceptionContextHolder; +import io.jeyong.detector.context.ExceptionContext; import io.jeyong.detector.context.QueryContextHolder; +import java.util.Optional; import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; -public class DetectNPlusOneExtension implements BeforeEachCallback, AfterEachCallback { +public class DetectNPlusOneExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback { + + private ExceptionContext exceptionContext; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext); + exceptionContext = getExceptionContext(applicationContext); + } + + private ExceptionContext getExceptionContext(ApplicationContext applicationContext) { + try { + return applicationContext.getBean(ExceptionContext.class); + } catch (Exception e) { + return null; + } + } @Override - public void beforeEach(ExtensionContext context) throws Exception { + public void beforeEach(ExtensionContext extensionContext) throws Exception { QueryContextHolder.clearContext(); - ExceptionContextHolder.clearContext(); + getOptionalExceptionContext().ifPresent(ExceptionContext::clearException); } @Override - public void afterEach(final ExtensionContext context) throws Exception { - try { - final NPlusOneQueryException primaryException = ExceptionContextHolder.getContext().getPrimaryException(); - if (primaryException != null) { - throw primaryException; + public void afterEach(final ExtensionContext extensionContext) throws Exception { + getOptionalExceptionContext().ifPresent(context -> { + try { + NPlusOneQueryException primaryException = context.getException(); + if (primaryException != null) { + throw primaryException; + } + } finally { + context.clearException(); } - } finally { - ExceptionContextHolder.clearContext(); - } + }); + } + + private Optional getOptionalExceptionContext() { + return Optional.ofNullable(exceptionContext); } } diff --git a/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java index 0b86ff4..8cbcf87 100644 --- a/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java +++ b/detector/src/main/java/io/jeyong/detector/test/NPlusOneTestConfig.java @@ -2,6 +2,7 @@ import io.jeyong.detector.config.NPlusOneDetectorConfig; import io.jeyong.detector.config.NPlusOneDetectorProperties; +import io.jeyong.detector.context.ExceptionContext; import io.jeyong.detector.template.NPlusOneQueryExceptionCollector; import io.jeyong.detector.template.NPlusOneQueryTemplate; import jakarta.annotation.PostConstruct; @@ -30,10 +31,15 @@ public void logInitialization() { logger.info("N+1 issues detected will throw exceptions."); } + @Bean + public ExceptionContext exceptionContext() { + return new ExceptionContext(); + } + @Primary @Bean - public NPlusOneQueryTemplate nPlusOneQueryExceptionCollector() { + public NPlusOneQueryTemplate nPlusOneQueryExceptionCollector(final ExceptionContext exceptionContext) { return new NPlusOneQueryExceptionCollector( - nPlusOneDetectorProperties.getThreshold()); + nPlusOneDetectorProperties.getThreshold(), exceptionContext); } } diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java index c76c066..3ab6a01 100644 --- a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java @@ -20,8 +20,8 @@ class AnnotationExceptionModeTest { @DisplayName("EXCEPTION 모드의 설정이 제대로 동작한다.") void testNPlusOneTestExceptionMode() { assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("5"); - assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.level")).isEqualTo("WARN"); assertThat(context.containsBean("nPlusOneQueryExceptionCollector")).isTrue(); + assertThat(context.containsBean("exceptionContext")).isTrue(); } } From 5ebc615ad0f64800a5effd0740dc9216555a48cd Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:25:07 +0900 Subject: [PATCH 09/14] =?UTF-8?q?test=20:=20@NPlusOneTest=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20API?= =?UTF-8?q?=EC=99=80=20=EB=B9=84=EC=A7=80=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=EC=84=9C=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/mode/AnnotationLoggingModeTest.java | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java index a051c8b..e721d86 100644 --- a/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java @@ -1,28 +1,71 @@ package io.jeyong.test.mode; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import io.jeyong.detector.test.NPlusOneTest; +import io.jeyong.test.case2.service.ProductService; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.AssertionsForInterfaceTypes; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.event.Level; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; +import org.springframework.http.ResponseEntity; -@SpringBootTest @NPlusOneTest(threshold = 3, level = Level.DEBUG, mode = NPlusOneTest.Mode.LOGGING) +@ExtendWith(OutputCaptureExtension.class) +@SpringBootTest( + webEnvironment = RANDOM_PORT, + properties = { + "logging.level.io.jeyong=debug", + }) class AnnotationLoggingModeTest { @Autowired - private ApplicationContext context; + private ApplicationContext applicationContext; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ProductService productService; @Test - @DisplayName("LOGGING 모드의 설정이 제대로 동작한다.") - void testNPlusOneTestLoggingMode() { + @DisplayName("LOGGING 모드의 설정이 동작한다.") + void testLoggingModeConfiguration() { assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("3"); assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.level")).isEqualTo("DEBUG"); - assertThat(context.containsBean("nPlusOneQueryLogger")).isTrue(); + assertThat(applicationContext.containsBean("nPlusOneQueryLogger")).isTrue(); + } + + @Test + @DisplayName("API 호출에서 LOGGING 모드가 동작한다.") + void testLoggingModeInApiCall(CapturedOutput output) { + String url = "http://localhost:" + port + "/api/products"; + ResponseEntity response = restTemplate.getForEntity(url, List.class); + + Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + Assertions.assertThat(output).contains("N+1 issue detected"); + } + + @Test + @DisplayName("Business Logic 호출에서 LOGGING 모드가 동작한다.") + void testLoggingModeInBusinessLogicCall(CapturedOutput output) { + productService.findAllProducts(); + + AssertionsForInterfaceTypes.assertThat(output).contains("N+1 issue detected"); } } From 53185d1155825f1f7008d095328fe31dd18077c4 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:37:38 +0900 Subject: [PATCH 10/14] =?UTF-8?q?test=20:=20@NPlusOneTest=EA=B0=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=84=A4=EC=A0=95=EC=97=90=EC=84=9C=20API?= =?UTF-8?q?=EC=99=80=20=EB=B9=84=EC=A7=80=EB=8B=88=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=EC=84=9C=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mode/AnnotationExceptionModeTest.java | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java index 3ab6a01..6ab5664 100644 --- a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java @@ -1,27 +1,84 @@ package io.jeyong.test.mode; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; import io.jeyong.detector.test.NPlusOneTest; +import io.jeyong.test.case2.service.ProductService; +import java.util.List; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.ApplicationContext; - -@SpringBootTest +import org.springframework.http.ResponseEntity; +// @formatter:off +/** + *

+ * 이 클래스는 {@code @NPlusOneTest} 어노테이션을 {@code EXCEPTION} 모드로 사용했을 때의 동작을 테스트합니다. + *

+ * + *

+ * {@code EXCEPTION} 모드에서는 N+1 쿼리 문제가 감지되면 테스트 메서드가 끝난 후 {@code afterEach}에서 예외가 발생합니다. + * 따라서 테스트 코드 내에서 예외를 잡아 직접 검증하는 것은 어렵습니다. + *

+ * + *

+ * 예외가 발생하는지 확인하려면 {@code threshold} 값을 낮게 설정(예: 2)하면 됩니다. + * 예를 들어, {@code threshold}를 2로 설정하면 동일한 쿼리가 2번 이상 실행될 때 {@code NPlusOneQueryException}이 발생하게 됩니다. + *

+ * + *

+ * 예시: + *

+ *
+ * {@code
+ * @NPlusOneTest(threshold = 2, mode = NPlusOneTest.Mode.EXCEPTION)
+ * }
+ * 
+ */ +// @formatter:on @NPlusOneTest(threshold = 5, mode = NPlusOneTest.Mode.EXCEPTION) +@SpringBootTest(webEnvironment = RANDOM_PORT) class AnnotationExceptionModeTest { @Autowired - private ApplicationContext context; + private ApplicationContext applicationContext; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ProductService productService; + @Test - @DisplayName("EXCEPTION 모드의 설정이 제대로 동작한다.") - void testNPlusOneTestExceptionMode() { + @DisplayName("EXCEPTION 모드의 설정이 동작한다.") + void testExceptionModeConfiguration() { assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("5"); - assertThat(context.containsBean("nPlusOneQueryExceptionCollector")).isTrue(); - assertThat(context.containsBean("exceptionContext")).isTrue(); + assertThat(applicationContext.containsBean("nPlusOneQueryExceptionCollector")).isTrue(); + assertThat(applicationContext.containsBean("exceptionContext")).isTrue(); + } + + @Test + @DisplayName("API 호출에서 EXCEPTION 모드가 동작한다.") + void testExceptionModeInApiCall() { + String url = "http://localhost:" + port + "/api/products"; + ResponseEntity response = restTemplate.getForEntity(url, List.class); + + Assertions.assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + @DisplayName("Business Logic 호출에서 EXCEPTION 모드가 동작한다.") + void testExceptionModeInBusinessLogicCall() { + productService.findAllProducts(); } } From 7d5ef6cf400151d003eb6314a306690ab6184854 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:41:59 +0900 Subject: [PATCH 11/14] =?UTF-8?q?chore=20:=20ExceptionContext=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=AC=20=EB=95=8C=20BeansException?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20=EC=9E=A1?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/jeyong/detector/test/DetectNPlusOneExtension.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java index 83682d2..cbdbe24 100644 --- a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java +++ b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -20,16 +21,16 @@ public void beforeAll(ExtensionContext extensionContext) throws Exception { exceptionContext = getExceptionContext(applicationContext); } - private ExceptionContext getExceptionContext(ApplicationContext applicationContext) { + private ExceptionContext getExceptionContext(final ApplicationContext applicationContext) { try { return applicationContext.getBean(ExceptionContext.class); - } catch (Exception e) { + } catch (BeansException e) { return null; } } @Override - public void beforeEach(ExtensionContext extensionContext) throws Exception { + public void beforeEach(final ExtensionContext extensionContext) throws Exception { QueryContextHolder.clearContext(); getOptionalExceptionContext().ifPresent(ExceptionContext::clearException); } From b419ae9fb3d13232b62ddca5be43a19839aa492f Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:43:47 +0900 Subject: [PATCH 12/14] =?UTF-8?q?chore=20:=20Callback=EC=9D=98=20implement?= =?UTF-8?q?s=20=EC=83=81=ED=99=A9=EC=97=90=EC=84=9C=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=8D=98=EC=A7=80=EC=A7=80=20=EC=95=8A=EC=95=84?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=9C=20=EA=B2=BD=EA=B3=A0=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/jeyong/detector/test/DetectNPlusOneExtension.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java index cbdbe24..5990106 100644 --- a/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java +++ b/detector/src/main/java/io/jeyong/detector/test/DetectNPlusOneExtension.java @@ -16,7 +16,7 @@ public class DetectNPlusOneExtension implements BeforeAllCallback, BeforeEachCal private ExceptionContext exceptionContext; @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { + public void beforeAll(ExtensionContext extensionContext) { ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext); exceptionContext = getExceptionContext(applicationContext); } @@ -30,13 +30,13 @@ private ExceptionContext getExceptionContext(final ApplicationContext applicatio } @Override - public void beforeEach(final ExtensionContext extensionContext) throws Exception { + public void beforeEach(final ExtensionContext extensionContext) { QueryContextHolder.clearContext(); getOptionalExceptionContext().ifPresent(ExceptionContext::clearException); } @Override - public void afterEach(final ExtensionContext extensionContext) throws Exception { + public void afterEach(final ExtensionContext extensionContext) { getOptionalExceptionContext().ifPresent(context -> { try { NPlusOneQueryException primaryException = context.getException(); From de07daf1f1d347447f802777ada7b4c5ca949b79 Mon Sep 17 00:00:00 2001 From: jeyong Date: Thu, 19 Sep 2024 07:53:01 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor=20:=20@NPlusOneTest=EC=9D=98=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=84=20System.getProperty=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20Properties=EB=A1=9C=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jeyong/test/mode/AnnotationExceptionModeTest.java | 8 +++++++- .../jeyong/test/mode/AnnotationLoggingModeTest.java | 11 ++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java index 6ab5664..76f1439 100644 --- a/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationExceptionModeTest.java @@ -3,10 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import io.jeyong.detector.config.NPlusOneDetectorProperties; import io.jeyong.detector.test.NPlusOneTest; import io.jeyong.test.case2.service.ProductService; import java.util.List; import org.assertj.core.api.Assertions; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -48,6 +50,9 @@ class AnnotationExceptionModeTest { @Autowired private ApplicationContext applicationContext; + @Autowired + private NPlusOneDetectorProperties nPlusOneDetectorProperties; + @LocalServerPort private int port; @@ -61,7 +66,8 @@ class AnnotationExceptionModeTest { @Test @DisplayName("EXCEPTION 모드의 설정이 동작한다.") void testExceptionModeConfiguration() { - assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("5"); + AssertionsForClassTypes.assertThat(nPlusOneDetectorProperties.isEnabled()).isTrue(); + AssertionsForClassTypes.assertThat(nPlusOneDetectorProperties.getThreshold()).isEqualTo(5); assertThat(applicationContext.containsBean("nPlusOneQueryExceptionCollector")).isTrue(); assertThat(applicationContext.containsBean("exceptionContext")).isTrue(); diff --git a/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java index e721d86..8fdd8f9 100644 --- a/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java +++ b/test/src/test/java/io/jeyong/test/mode/AnnotationLoggingModeTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import io.jeyong.detector.config.NPlusOneDetectorProperties; import io.jeyong.detector.test.NPlusOneTest; import io.jeyong.test.case2.service.ProductService; import java.util.List; @@ -26,13 +27,16 @@ @SpringBootTest( webEnvironment = RANDOM_PORT, properties = { - "logging.level.io.jeyong=debug", + "logging.level.io.jeyong=debug" }) class AnnotationLoggingModeTest { @Autowired private ApplicationContext applicationContext; + @Autowired + private NPlusOneDetectorProperties nPlusOneDetectorProperties; + @LocalServerPort private int port; @@ -45,8 +49,9 @@ class AnnotationLoggingModeTest { @Test @DisplayName("LOGGING 모드의 설정이 동작한다.") void testLoggingModeConfiguration() { - assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.threshold")).isEqualTo("3"); - assertThat(System.getProperty("spring.jpa.properties.hibernate.detector.level")).isEqualTo("DEBUG"); + assertThat(nPlusOneDetectorProperties.isEnabled()).isTrue(); + assertThat(nPlusOneDetectorProperties.getThreshold()).isEqualTo(3); + assertThat(nPlusOneDetectorProperties.getLevel()).isEqualTo(Level.DEBUG); assertThat(applicationContext.containsBean("nPlusOneQueryLogger")).isTrue(); } From 6748f9bc1849d961d22c6fe500ada77b7efe8fd4 Mon Sep 17 00:00:00 2001 From: jeyong <118044367+joon6093@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:58:56 +0900 Subject: [PATCH 14/14] =?UTF-8?q?chore=20:=20=EB=A6=B4=EB=A6=AC=EC=A6=88?= =?UTF-8?q?=20=EB=B2=84=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- detector/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detector/build.gradle b/detector/build.gradle index d748604..3ecf9e1 100644 --- a/detector/build.gradle +++ b/detector/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'io.jeyong' -version = '2.0.0' +version = '2.0.1' java { toolchain {