[자바] JMH(Java Micromenchmark Harness)

JMH

JMH(Java Micromenchmark Harness)는 자바 및 JVM을 대상으로 하는 언어로 작성된 마이크로 벤치마크 코드를 빌드, 실행 및 분석하기 위한 마이크로벤치마크(micromenchmark) 프레임워크이다.


스프링 프레임워크에서 JMH 라이브러리 사용

JMH 코어와 JMH 어노테이션 프로세서 라이브러리 의존성을 추가한다. 테스트 프레임워크와 통합할 수도 있다.

dependencies {
  // jmh
  implementation("org.openjdk.jmh:jmh-core:1.35")
  annotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.35")
  testImplementation("org.openjdk.jmh:jmh-core:1.35")
  testAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.35")
}


그래들 jmh 플러그인

그래들 jmh 플러그인(https://github.com/melix/jmh-gradle-plugin)를 사용함으로써 프레임워크를 그래들 빌드 시스템과 통합할 수 있다. 이 플러그인을 사용하면 별도의 구성을 통해 기존 프로젝트 소스와 jmh의 통합을 쉽게 할 수 있으며 벤치마크 코드 빌드 및 실행을 위한 태스크를 사용할 수 있다.

플러그인을 사용하기 위해 build.gradle 파일에 다음과 같이 정의한다.

plugins {
  id "me.champeau.jmh" version "0.7.0"
}

벤치마크 소스 파일은 src/jmh 경로에 위치시킨다.

src/jmh
     |- java      : 벤치마크 대상 자바 소스 파일
     |- resources : 벤치마크를 위해 필요한 리소스 파일

플러그인은 벤치마크 소스 파일이 서드파티 라이브러리에 의존하는 경우 해당 라이브러리를 위해 사용해야 하는 jmh 구성을 만든다. 따라서 해당 라이브러리의 의존성을 다음과 같이 추가하면 된다.

dependencies {
    jmh 'commons-io:commons-io:2.4'
}

그래들의 jmh 플러그인을 사용하면 별도의 프로젝트를 만들지 않고도 기존 소스를 쉽게 테스트할 수 있다. 이를 위해서는 벤치마크 대상 소스 파일을 src/main/java 대신 src/jmh/java에 위치시켜야 한다.

jmh(벤치마크) 태스크는 기본적으로 main(프로덕션) 소스 셋(source set)에 의존한다. jmh 구성 블록 내에서 includeTests 프로퍼티를 true로 설정하여 jmh 태스크가 test(테스트) 소스 셋에 의존하도록 만들 수 있다.

기본적으로 모든 벤치마크가 실행된 후 결과는 $buildDir/reports/jmh에 생성된다.


벤치마크 구성

벤치마크 모드

벤치마크 대상 메서드를 다양한 모드로 측정할 수 있다. 메서드에 @BenchmarkMode를 설정함으로써 모드 설정이 가능하다. 설정 가능한 벤치마크 모드 속성은 다음과 같다.

  1. 처리량 (throughput): 벤치마크 메서드를 제한된 시간에 반복적으로 계속 호출하고 메서드가 실행된 횟수를 계산하여 처리량을 측정한다.
  2. 평균 시간 (average time): 처리량과 유사한 방식으로 평균 실행 시간을 측정한다. 처리량 모드와 상호적으로 볼 수 있다.
  3. 실행 시간 샘플링 (sample time): 실행 시간을 샘플링하는 모드이다. 이 모드도 제한된 시간에 반복적으로 벤치마크 메서드를 실행하지만 전체 시간을 측정하는 대신 메서드 호출 중 일부에 대해 소요된 시간을 측정한다. 이를 통해 분포, 백분위수 등을 유추할 수 있다. 메서드 실행 시간이 충분히 길다면 모든 샘플이 캡처되므로 JMH는 샘플링 빈도를 자동으로 조정하려고 시도한다.
  4. 단일 호출 시간 (single shot time): 단일 메서드 호출 시간을 측정한다. 단일 벤치마크 메서드 호출만 수행한다. 이 모드에서는 벤치마크 메서드가 중지되는 즉시 반복이 끝나므로 반복 시간이 의미가 없다. 이 모드는 벤치마크 메서드를 계속 호출하지 않으려는 경우 콜드 스타트업 테스트를 수행할 때 유용하다.


상태

대부분의 경우 벤치마크가 실행되는 동안 객체는 일정한 상태를 유지해야 한다. JMH는 멀티스레드를 사용한 동시성 벤치마크에 많이 사용되기 때문에 객체의 상태를 유지하기 위한 명시적인 개념이 존재한다. 멀티스레드 환경에서 선언한 모든 벤치마크 메서드는 모든 벤치마크 스레드 상에서 실행된다.

객체는 필요에 따라 인스턴스화되고 전체 벤치마크 테스트 중에 재사용된다. 객체는 항상 해당 객체에 대한 접근 권한을 갖는 벤치마크 스레드 중 하나에 의해 인스턴스화된다.

@State를 설정한 클래스를 벤치마크 메서드의 파라미터로 정의하면 벤치마크 메서드가 상태를 참조할 수 있다. 메서드 호출 시 JMH는 적절한 상태를 주입한다.

벤치마크 클래스에 @State를 설정함으로써 객체의 상태 설정이 가능하다. 객체가 유지하는 상태는 다음 두 가지가 있다.

  1. 벤치마크 (benchmark): 모든 벤치마크 스레드는 상태 인스턴스를 공유한다. 상태가 공유되는 벤치마크가 수행된다.
  2. 스레드 (thread): 각각의 벤치마크 스레드는 상태 인스턴스 복사본을 가진다. 상태가 공유되지 않는 벤치마크가 수행된다.


인텔리제이의 JMH 플러그인

JMH를 인텔리제이(IntelliJ) IDE 상애서 JUnit과 동일한 방식으로 사용할 수 있게 해주는 플러그인(https://github.com/artyushov/idea-jmh-plugin/)이 존재한다. 이를 사용하여 벤치마크 테스트를 IDE 상에서 보다 손쉽게 할 수 있다.

이 플러그인은 벤치마크 메서드(@Benchmark이 설정된 메서드) 자동 생성, 벤치마크 메서드를 각각 실행, 클래스 내 모든 벤치마크 메서드 실행 기능을 IDE 상의 플러그인 기능으로 사용할 수 있게 해준다.


스프링 프레임워크에서 JMH를 사용하여 테스트 메서드 벤치마크

JMH를 스프링의 테스트 컨텍스트 프레임워크와 통합하여 테스트 대상 메서드를 별도로 분리하여 작성한 후 벤치마크 테스트를 할 수 있다.

테스트 클래스에서 JMH를 사용하는 방법은 다음과 같다.

  1. JMH 어노테이션을 테스트 클래스 및 메서드에 사용한 후 그레이들 JMH 플러그인을 사용하여 벤치마크
  2. JMH 어노테이션을 테스트 클래스 및 메서드에 사용한 후 인텔리제이의 JMH 플러그인을 사용하여 벤치마크
  3. JMH 어노테이션을 테스트 클래스 및 메서드에 사용한 후 JMH의 Runner 객체를 사용하여 벤치마크

1번의 경우 여러 테스트 클래스에 정의된 벤치마크 대상 메서드를 한 번에 실행할 수 있다. 벤치마크 실행 속성 설정은 JMH 어노테이션 속성 또는 플러그인 속성 설정을 통해 가능하다. 플러그인 속성 설정을 통해 원하는 테스트 메서드만 실행할 수 있지만 별도의 파일 변경이 필요하여 다소 번거롭다.

jmh {
  includes = ['some regular expression']
  warmupIterations = 1
  iterations = 10
  benchmarkMode = ['thrpt','ss']
  batchSize = 1
  fork = 2
  threads = 4
}

2번의 경우 여러 테스트 클래스에 정의된 벤치마크 대상 메서드를 한 번에 실행할 수 없지만 원하는 테스트 메서드를 선택적으로 실행할 수 있다. 벤치마크 실행 속성 설정은 JMH 어노테이션 속성 설정을 통해 가능하다.

@SpringBootTest
@RunWith(SpringRunner.class)
@State(Scope.Benchmark)
public class JmhTest {
  @Benchmark
  @BenchmarkMode(Mode.Throughput)
  @OutputTimeUnit(TimeUnit.SECONDS) 
  @Fork(0)
  @Threads(10)
  @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
  @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
  public void measure() {
    ...
  }
}

3번의 경우 테스트 메서드(@Test)에서 JMH의 Runner 인스턴스화 및 JMH 속성을 설정한 후 벤치마크를 실행하는 방법이다. 하나의 테스트 클래스에 정의된 벤치마크 대상 메서드를 한 번에 일괄적으로 실행할 수 있으며 원하는 테스트 메서드를 선택적으로 실행할 수도 있다. 벤치마크 실행 속성 설정은 OptionsBuilder의 메서드 호출이나 JMH 어노테이션 속성 설정을 통해 가능하다.

@SpringBootTest
@RunWith(SpringRunner.class)
@State(Scope.Benchmark)
public class JmhTest {
  @Test
  public void executeJmhRunner() {
    Options jmhRunnerOptions = new OptionsBuilder()
      .include("\\." + this.getClass().getSimpleName() + "\\.")
      .warmupIterations(3)
      .measurementIterations(3)
      .forks(0)
      .threads(1)
      .build();

       new Runner(jmhRunnerOptions).run();
  }

  @Benchmark
  @BenchmarkMode(Mode.Throughput)
  @OutputTimeUnit(TimeUnit.SECONDS) 
  @Fork(0)
  @Threads(10)
  @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
  @Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)
  public void measure() {
    ...
  }
}


의존성 주입

내부적으로 JMH의 Runner는 벤츠마크 실행 시 새로운 스레드를 생성하므로 @Benchmark을 적용한 JMH 벤치마크 테스트 메서드는 @Autowired를 사용하여 의존성을 필드 주입한 객체에 접근할 수 없다(null 참조가 된다).

테스트 클래스에 객체의 의존성을 주입한 후 JMH 벤치마크 테스트 메서드에서 해당 객체에 접근하기 위해서는 별도의 방법이 필요하다. 다음 두 방법을 사용할 수 있다.

  1. @Setup 적용 메서드에서 사용하려는 객체를 수동으로 인스턴스화한다.
  2. 객체를 정적 필드로 선언한 후 수정자(setter) 주입을 사용한다.

위 방법으로 객체를 적절히 의존성 주입하였다면 추가적으로 Runner의 포크(fork) 옵션을 0으로 설정해야 한다. 해당 옵션을 0으로 설정하지 않으면 객체의 의존성 주입을 적절히 하였더라도 @Benchmark 적용 메서드는 테스트 클래스의 필드에 접근할 수 없다.

생성자 주입을 사용하려는 경우 @State을 기본 public 생성자가 있는 클래스에만 적용할 수 있다는 에러가 발생하게 된다. 그러나 Junit 5 테스트 프레임워크 사용 시 테스트 클래스에 하나의 생성자만 정의할 수 있으므로 생성자를 사용한 의존 객체 주입이 불가능하며 따라서 수정자 주입을 사용한다.


트랜잭션

JMH를 스프링의 테스트 컨텍스트 프레임워크와 통합하고 테스트 대상 메서드를 별도로 분리하여 작성한 후 테스트할 수 있다. 이때 테스트 클래스에서 JMH를 사용하여 데이터베이스와 연결된 서비스를 슬라이스 및 통합 테스트하는 경우 테스트 관리(test-managed) 트랜잭션 내에서 테스트 코드가 수행되도록 함으로써 적절한 트랜잭션 관리가 이루어지도록 할 수 있다.

테스트 컨텍스트 프레임워크에서 관리되는 트랜잭션(테스트 관리 트랜잭션)은 스프링 또는 애플리케이션에 의해 관리되는 트랜잭션과 다르지만 스프링 관리 및 애플리케이션 관리 트랜잭션은 일반적으로 테스트 관리 트랜잭션에 참여하므로 테스트 시에도 스프링이 제공하는 추상화된 트랜잭션 관리 기능을 사용할 수 있다.


참고

Categories: ,

Updated:

Comments