TestExecutionListener 사용 시 OutOfMemoryError 발생 트러블슈팅

appti·2024년 2월 17일
0

트러블슈팅

목록 보기
1/3

서론

프로젝트에서 @SpringBootTest를 사용하는 통합 테스트, 인수 테스트에서 DB를 격리시키기 위해 TestExecutionListener를 사용하고 있었습니다.

public class DatabaseCleanListener extends AbstractTestExecutionListener {

    @Override
    public void afterTestMethod(final TestContext testContext) {
        final EntityManager em = findEntityManager(testContext);
        final List<String> tableNames = calculateTableNames(em);

        clean(em, tableNames);
    }

    ...

    private void clean(final EntityManager em, final List<String> tableNames) {
      em.flush();
      em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

      for (final String tableName : tableNames) {
          em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
          em.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN id RESTART WITH 1").executeUpdate();
      }

      em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
  }
}

로직을 리펙토링하던 도중, 특정 테스트에서 OutOfMemoryError가 발생했습니다.

문제 상황

10회 이상 재실행했으며, 동일한 테스트 순서, 동일한 테스트에서 OutOfMemoryError가 발생했습니다.
시간이 지날수록 Tomcat, HikariCP 등 다양한 스레드에서 OutOfMemoryError가 발생했습니다.

단독으로 실행한 결과 정상적으로 실행됨을 확인할 수 있었습니다.

문제 시점 파악

Hibernate: 
    /* dynamic native SQL query */ 
SET
    REFERENTIAL_INTEGRITY FALSE
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE auction
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE auction ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE review
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE review ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE users
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE users ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE user_reliability
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE user_reliability ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE auction_region
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE auction_region ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE question
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE question ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE black_list_token
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE black_list_token ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE chat_room_report
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE chat_room_report ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE answer_report
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE answer_report ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE read_message_log
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE read_message_log ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE auction_report
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE auction_report ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE auction_image
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE auction_image ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE reliability_update_history
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE reliability_update_history ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE chat_room
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE chat_room ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE answer
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE answer ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE region
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE region ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE profile_image
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE profile_image ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE device_token
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE device_token ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE bid
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE bid ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE message
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE message ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE question_report
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE question_report ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE categories
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE categories ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ 
SET
    REFERENTIAL_INTEGRITY TRUE

정상적으로 DB 격리를 수행했을 때의 로그입니다.

Hibernate: 
    /* dynamic native SQL query */ 
SET
    REFERENTIAL_INTEGRITY FALSE
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE question
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE question ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE bid
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE bid ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE chat_room_report
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE chat_room_report ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE answer
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE answer ALTER COLUMN id RESTART WITH 1
Hibernate: 
    /* dynamic native SQL query */ TRUNCATE TABLE profile_image
Hibernate: 
    /* dynamic native SQL query */ ALTER TABLE profile_image ALTER COLUMN id RESTART WITH 1
Exception in thread "Catalina-utility-1" java.lang.OutOfMemoryError: Java heap space

OutOfMemory가 발생했을 때의 DB 격리 로그입니다.
일부 테이블에 대한 격리를 수행(TRUNCATE + PK 1 초기화)하다가 OutOfMemoryError가 발생한 것을 확인할 수 있었습니다.

이로 인해 일단 TestExecutionListener와 관련된 문제라는 추측을 했습니다.
확실히 파악하기 위해 Heap Dump를 분석하고자 했습니다.

Heap Dump

테스트 실행 중 발생했으므로, gradlew를 통해 실행한 프로세스를 확인한 뒤 Heap Dump 파일을 생성하고자 했습니다.

jps

5796 GradleWorkerMain
4775 Main
5800 Jps
1224 GradleDaemon
5784 GradleWrapperMain
4010 GradleDaemon

jmap -dump:format=b,file=testdump.hprof 5796

Eclipse MAT을 활용한 Heap Dump 분석

Eclipse MAT으로 분석한 결과, 하나의 원인을 발견했습니다.

원인이 InvocationContainerImpl으로 나왔습니다.

InvocationContainerImpl은 Mockito에서 메서드 호출 정보를 관리하는 역할로, Stub 설정이나 Verification을 지원합니다.
테스트 도중 OutOfMemoryError가 발생했으니 맞는 말이기는 하겠지만, 이걸로는 너무 추상적인 정보라고 생각했습니다.

Test worker
  at java.util.jar.JarFile.ensureInitialization()V (JarFile.java:1026)
  at java.util.jar.JavaUtilJarAccessImpl.ensureInitialization(Ljava/util/jar/JarFile;)V (JavaUtilJarAccessImpl.java:72)
  at jdk.internal.loader.URLClassPath$JarLoader$2.getManifest()Ljava/util/jar/Manifest; (URLClassPath.java:883)
  at jdk.internal.loader.BuiltinClassLoader.defineClass(Ljava/lang/String;Ljdk/internal/loader/Resource;)Ljava/lang/Class; (BuiltinClassLoader.java:848)
  at jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Ljava/lang/String;)Ljava/lang/Class; (BuiltinClassLoader.java:760)
  at jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Ljava/lang/String;Z)Ljava/lang/Class; (BuiltinClassLoader.java:681)
  at jdk.internal.loader.BuiltinClassLoader.loadClass(Ljava/lang/String;Z)Ljava/lang/Class; (BuiltinClassLoader.java:639)
  at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Ljava/lang/String;Z)Ljava/lang/Class; (ClassLoaders.java:188)
  at java.lang.ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class; (ClassLoader.java:520)
  at org.junit.platform.launcher.core.OutcomeDelayingEngineExecutionListener.executionFinished(Lorg/junit/platform/engine/TestDescriptor;Lorg/junit/platform/engine/TestExecutionResult;)V (OutcomeDelayingEngineExecutionListener.java:59)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.reportCompletion()V (NodeTestTask.java:195)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute()V (NodeTestTask.java:100)
  at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(Lorg/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorService$TestTask;)Ljava/util/concurrent/Future; (SameThreadHierarchicalTestExecutorService.java:35)
  at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute()Ljava/util/concurrent/Future; (HierarchicalTestExecutor.java:57)
  at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(Lorg/junit/platform/engine/ExecutionRequest;)V (HierarchicalTestEngine.java:54)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(Lorg/junit/platform/engine/TestDescriptor;Lorg/junit/platform/engine/EngineExecutionListener;Lorg/junit/platform/engine/ConfigurationParameters;Lorg/junit/platform/engine/TestEngine;)V (EngineExecutionOrchestrator.java:107)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(Lorg/junit/platform/launcher/core/LauncherDiscoveryResult;Lorg/junit/platform/engine/EngineExecutionListener;)V (EngineExecutionOrchestrator.java:88)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(Lorg/junit/platform/launcher/core/InternalTestPlan;Lorg/junit/platform/launcher/core/LauncherDiscoveryResult;Lorg/junit/platform/launcher/TestExecutionListener;)V (EngineExecutionOrchestrator.java:54)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator$$Lambda$254+0x0000000800d7bd08.accept(Ljava/lang/Object;)V ()
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(Lorg/junit/platform/engine/ConfigurationParameters;Lorg/junit/platform/launcher/core/ListenerRegistry;Ljava/util/function/Consumer;)V (EngineExecutionOrchestrator.java:67)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(Lorg/junit/platform/launcher/core/InternalTestPlan;[Lorg/junit/platform/launcher/TestExecutionListener;)V (EngineExecutionOrchestrator.java:52)
  at org.junit.platform.launcher.core.DefaultLauncher.execute(Lorg/junit/platform/launcher/core/InternalTestPlan;[Lorg/junit/platform/launcher/TestExecutionListener;)V (DefaultLauncher.java:114)
  at org.junit.platform.launcher.core.DefaultLauncher.execute(Lorg/junit/platform/launcher/LauncherDiscoveryRequest;[Lorg/junit/platform/launcher/TestExecutionListener;)V (DefaultLauncher.java:86)
  at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(Lorg/junit/platform/launcher/LauncherDiscoveryRequest;[Lorg/junit/platform/launcher/TestExecutionListener;)V (DefaultLauncherSession.java:86)
  at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses()V (JUnitPlatformTestClassProcessor.java:110)
  at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(Lorg/gradle/api/internal/tasks/testing/junitplatform/JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor;)V (JUnitPlatformTestClassProcessor.java:90)
  at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop()V (JUnitPlatformTestClassProcessor.java:85)
  at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop()V (SuiteTestClassProcessor.java:62)
  at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (NativeMethodAccessorImpl.java(Native Method))
  at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (NativeMethodAccessorImpl.java:77)
  at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (Method.java:568)
  at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(Lorg/gradle/internal/dispatch/MethodInvocation;)V (ReflectionDispatch.java:36)
  at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(Ljava/lang/Object;)V (ReflectionDispatch.java:24)
  at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(Ljava/lang/Object;)V (ContextClassLoaderDispatch.java:33)
  at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object; (ProxyDispatchAdapter.java:94)
  at jdk.proxy2.$Proxy5.stop()V ()
  at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run()V (TestWorker.java:193)
  at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(Ljava/lang/Runnable;)V (TestWorker.java:129)
  at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(Lorg/gradle/process/internal/worker/WorkerProcessContext;)V (TestWorker.java:100)
  at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(Ljava/lang/Object;)V (TestWorker.java:60)
  at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(Lorg/gradle/process/internal/worker/WorkerProcessContext;)V (ActionExecutionWorker.java:56)
  at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call()Ljava/lang/Void; (SystemApplicationClassLoaderWorker.java:113)
  at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call()Ljava/lang/Object; (SystemApplicationClassLoaderWorker.java:65)
  at worker.org.gradle.process.internal.worker.GradleWorkerMain.run()V (GradleWorkerMain.java:69)
  at worker.org.gradle.process.internal.worker.GradleWorkerMain.main([Ljava/lang/String;)V (GradleWorkerMain.java:74)

다른 정보를 확인해도 구체적인 원인은 파악할 수 없었습니다.

VisualVM을 활용한 Heap Dump 분석

VisualVM은 OutOfMemoryError가 발생한 경우 Summary에 이를 표기해줍니다.

하지만 Heap Dump를 분석한 결과를 보니 아쉽게도 나오지 않았습니다.
원인에 대해서는 gradle을 통해 수행한 테스트이며, 기준이 없기 때문에 섣부르게 판단할 수 없었습니다.

가설

아무 가설도 없이 Heap Dump를 파악해본 결과 구체적인 원인을 찾을 수 없었습니다.
문제의 범위를 좁히기 위해 다시 코드로 돌아갔습니다.

@Override
public void afterTestMethod(final TestContext testContext) {
    final EntityManager em = findEntityManager(testContext);
    final List<String> tableNames = calculateTableNames(em);

    clean(em, tableNames);
}

private void clean(final EntityManager em, final List<String> tableNames) {
    em.flush();
    em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

    for (final String tableName : tableNames) {
        em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
        em.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN id RESTART WITH 1").executeUpdate();
    }

    em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}

OutOfMemoryError는 결국 JVM의 힙 메모리가 부족하다는 의미입니다.
현재 200개 이상의 테스트 수행 이후 DB 격리 로직이 동작하기 때문에, GC가 필요한 객체들이 많을 것입니다.
그래서 GC가 정상적으로 수행되지 않거나 부족한 상황이라고 판단했습니다.

GC를 모니터링하려고 했지만 VisualVM 이슈로 실행 중인 프로세스에 대한 분석이 불가능했기 때문에 직접 확인하지 못했습니다.

그래서 몇 가지 가설을 세우고, 직접 테스트를 진행했습니다.

가설 1 - 참조되지 않는 Query 최적화

private void clean(final EntityManager em, final List<String> tableNames) {
    em.flush();

    final StringBuilder sb = new StringBuilder("SET REFERENTIAL_INTEGRITY FALSE;");

    for (final String tableName : tableNames) {
        sb.append("TRUNCATE TABLE ")
          .append(tableName)
          .append(";");

        sb.append("ALTER TABLE ")
          .append(tableName)
          .append(" ALTER COLUMN id RESTART WITH 1;");
    }

    sb.append("SET REFERENTIAL_INTEGRITY TRUE;");

    em.createNativeQuery(sb.toString()).executeUpdate();
}

em.createNativeQuery()는 Query(NativeQueryImpl)를 반환합니다.

기존에는 모든 쿼리를 매번 em.createNativeQuery()로 생성했지만, 이를 StringBuilder를 통해 모든 쿼리를 하나의 문자열로 만든 뒤 실행하도록 변경했습니다.

일단 테스트가 정상적으로 실행되는 것을 확인할 수 있었습니다.

@Test
void 리뷰어를_찾을_수_없다면_예외가_발생한다() {
    // when & then
    assertThatThrownBy(() -> reviewService.create(존재하지_않는_사용자가_평가를_등록하려는_DTO))
            .isInstanceOf(ReviewerNotFoundException.class);

    while(true) {
    }
}

비교를 위해 테스트가 실패한 구간에 무한 반복하도록 세팅하고, 그 시점에 Heap Dump를 생성해 NativeQueryImpl을 비교해보고자 했습니다.

첫 번째는 기존 코드일 때이며, 두 번째는 변경했을 때의 객체 수입니다.

그 차이가 꽤 큰 것을 확인할 수 있었고, 이를 통해 OutOfMemory를 해결할 수 있다고 판단했습니다.

가설 2 - DB 격리 실행 시점에 의한 문제

@Override
public void beforeTestMethod(final TestContext testContext) {
    final EntityManager em = findEntityManager(testContext);
    final List<String> tableNames = calculateTableNames(em);

    clean(em, tableNames);
}

private void clean(final EntityManager em, final List<String> tableNames) {
    em.flush();
    em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

    for (final String tableName : tableNames) {
        em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
        em.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN id RESTART WITH 1").executeUpdate();
    }

    em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}

afterTestMethod()를 beforeTestMethod()로 변경한 결과 테스트 수행에 성공했습니다.

이 또한 NativeQueryImpl가 영향이 있는지 확인해봤습니다.

위는 afterMethodTest(), 아래는 beforeMethodTest()의 결과입니다.

count/size가 동일하므로 이는 큰 상관이 없다고 판단했습니다.

위는 afterMethodtest(), 아래는 beforeMethodTest()의 결과입니다.
Heap Size는 유의미한 차이를 보이고 있었습니다.

전반적으로 객체가 차지하고 있는 크기가 줄어든 것을 확인할 수 있었습니다.
즉, beforeTestMethod()를 통해 테스트 수행 전 DB 격리를 했기 때문에 그에 의해 GC가 수월하게 진행되었다고 판단했습니다.

전체적으로 유의미한 차이를 보이고 있었기 때문에 beforeTestMethod()로 변경해 OutOfMemory를 해결할 수 있다고 판단했습니다.

결론

코드 변경

public class DatabaseCleanListener extends AbstractTestExecutionListener {

    @Override
    public void beforeTestMethod(final TestContext testContext) {
        final EntityManager em = findEntityManager(testContext);
        final List<String> tableNames = calculateTableNames(em);

        clean(em, tableNames);
    }
    
    ...
    
    private void clean(final EntityManager em, final List<String> tableNames) {
        em.flush();

        final StringBuilder sb = new StringBuilder("SET REFERENTIAL_INTEGRITY FALSE;");

        for (final String tableName : tableNames) {
            sb.append("TRUNCATE TABLE ")
              .append(tableName)
              .append(";");

            sb.append("ALTER TABLE ")
              .append(tableName)
              .append(" ALTER COLUMN id RESTART WITH 1;");
        }

        sb.append("SET REFERENTIAL_INTEGRITY TRUE;");

        em.createNativeQuery(sb.toString()).executeUpdate();
    }
}

위와 같이 코드를 변경했습니다.

테스트 시 주의사항

  • TestExecutionListener로 DB 격리 시 beforeTestMethod()가 더 안전합니다.
    • 혹은 beforeTestMethod()와 afterTestMethod()를 모두 사용하면 안전하게 테스트 전처리 및 후처리가 가능합니다.
  • 테스트가 많거나 테스트 시 사용하는 인스턴스가 많을 경우 이를 처리할 방법을 고려하거나, 효율적인 코드를 작성할 필요가 있습니다.
profile
안녕하세요

0개의 댓글