들어가며
최근에 진행했던 프로젝트에서 사용했던 메서드의 성능을 개선시킨 것 같은데, 정확히 얼마나 성능이 향상된 것인지 비교를 해보고 싶었습니다. 메서드 수행 시간을 측정하기 위해 여러 레퍼런스를 찾아봤는데, 그중에서 JMH(Java Microbenchmark Harness)을 활용하는 것이 가장 현대적이고 효율적이라고 생각되었습니다.
본 글에서는 gradle 환경에서 JMH를 활용하여 유닛 단위로 벤치마킹하는 방법에 대해 정리하겠습니다. 구글링해보면 대개 maven 환경에서 JMH를 많이 사용하고, gradle 환경 기반의 JMH 환경 설정 글은 별로 없었습니다. 그래서 gradle 환경에서 최대한 간단하게 JMH 환경 설정하는 방법을 적고자 했습니다.
개발 환경은 다음과 같습니다.
- OS : Windows 10
- Gradle : 6.8.3
- IDE : Intellij Ultimate 2020.3.2
- JDK : 11
본 글에서 JMH을 사용하기 위해 적용하는 플러그인은 me.champeau.jmh입니다. 관련 정보는 아래와 같습니다.
본론
1. IntelliJ IDE에서 New Project - Gradle - JDK 설정 및 Java 추가
2. 프로젝트명, 그룹명, 아티팩트명 설정
3. IDE에서 자동으로 gradle을 build한 후 프로젝트 구조는 다음과 같습니다. 다른 건 신경 쓸 필요 없고 src 디렉토리만 보면 됩니다.
src 디렉토리 하위에 jmh라는 디렉토리를 하나 만들어주시고, 아래 그림과 같이 main에 있는 java 디렉토리와 resources 디렉토리를 jmh 디렉토리로 옮겨주세요.
그리고 사용하지 않을 main 디렉토리와 test 디렉토리는 제거해줍시다. 굳이 제거해주지 않아도 되긴 하는데, 그냥 깔끔하게 지워주겠습니다. 위 과정을 모두 정상적으로 수행하셨다면 아래와 같은 프로젝트 구조가 되어있으셔야 합니다. 참고로 이 디렉토리 구조는 저희가 사용할 플러그인에서 권장하는 디렉토리 구조입니다.
4. 대부분 gradle 환경에서 JMH을 편하게 사용할 수 있도록 만들어져있는 플러그인을 사용하므로 저 또한 그것을 활용하겠습니다. 저는 최신 버전인 0.6.2를 사용하겠습니다. 플러그인의 README에 따르면 0.6 이상의 버전을 사용하기 위해서는 gradle의 버전은 6.8 이상이어야 합니다.
gradle의 버전을 확인하기 위해 gradle/wrapper/gradle-wrapper.properties를 보겠습니다.
제 기준, IntelliJ에서 프로젝트를 새로 생성할 때 gradle 버전을 6.7로 만들어줍니다. 혹시 6.8 이상의 버전이 만들어진다면 그대로 두시고 만약 6.8 미만의 버전의 gradle일 경우 6.8 이상의 LTS버전으로 바꿔줍시다. 글을 작성하는 2021년 3월 22일 기준, LTS는 6.8.3이므로 아래와 같이 gradle 버전을 수정해주겠습니다.
5. 이제 build.gradle을 수정해주겠습니다.
수정 전 build.gradle
plugins {
id 'java'
}
group 'monggu'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
test {
useJUnitPlatform()
}
수정 후 build.gradle
plugins {
id 'java'
id "me.champeau.jmh" version "0.6.2"
}
group 'monggu'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
test {
useJUnitPlatform()
}
수정 후 파일은 별거 없고, plugins에 아래와 같이 me.champeau.jmh 플러그인 관련 정보 한 줄만 추가해주면 끝입니다. 여기까지만 해줘도 전혀 문제 없이 JMH를 사용할 수 있습니다.
아울러 build.gradle 관련하여 두 가지 설명을 덧붙이겠습니다. 하나는 플러그인에서 사용하고 있는 JMH의 버전을 임의로 변경하는 방법, 또 하나는 JMH 관련 설정 방법입니다. 특히 JMH 관련 설정 방법은 꼭 보시길 권장드립니다. 안 그러면 되는지 안 되는지 테스트해보려고 시도했다가 괜히 수십 분이 날아갈 수 있습니다.
JMH의 버전을 임의로 변경하는 방법
플러그인 README에 따르면 0.6.2 버전의 플러그인은 JMH 1.28 버전을 사용하고 있습니다. 글을 작성하는 현재 기준 JMH의 LTS는 1.28입니다. 플러그인에서 JMH의 최신 버전을 반영해주고 있네요! 업데이트가 잘 이루어지고 있는 것 같습니다. 혹 플러그인의 버전 업데이트가 제대로 이루어지지 않아서 JMH의 최신 버전이 반영이 안되었다면 직접 반영해줄 수도 있습니다. build.gradle에서 아래와 같이 dependencies 블록을 수정해주면 됩니다. 참고로 이 예시는 플러그인 README에 있는 코드인데, 엄밀히 말하면 현재 버전보다 다운그레이드시키고 있습니다ㅋㅋㅋ ㅎㅎ 그냥 예시라고 생각해주시고, 버전만 적절하게 수정해주시면 됩니다.
...
dependencies {
...
jmh 'org.openjdk.jmh:jmh-core:0.9'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:0.9'
}
...
JMH 관련 설정 방법
위에서 보여드렸던 수정 후의 build.gradle 코드에서 다른 건 전혀 건드리지 마시고 아래와 같이 jmh 블록을 추가해줍니다.
...
jmh {
}
위 블록 안에는 본인이 희망하는 JMH 관련 설정을 넣어주시면 됩니다. 넣을 수 있는 설정은 플러그인의 README를 참고해주세요.
본 글에서는 환경 설정을 하고 본인의 개발 환경에서 정상 작동하는지가 궁금한 것이니 가장 빠르게 벤치마킹을 끝낼 수 있는 설정으로 바꾸겠습니다.
jmh {
fork = 1
warmupIterations = 1
iterations = 1
}
위의 모든 과정을 거치고 최종적으로는 다음과 같은 build.gradle 파일이 작성됩니다. 급하신 분들은 이것만 긁어가셔도 됩니다.
최종 build.gradle
plugins {
id 'java'
id "me.champeau.jmh" version "0.6.2"
}
group 'monggu'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
test {
useJUnitPlatform()
}
jmh {
fork = 1
warmupIterations = 1
iterations = 1
}
6. 이제 벤치마킹할 소스코드를 작성해보겠습니다. 우선 src/jmh/java 하위에 적절한 이름의 패키지를 만들어주시고, 그 안에 적절한 이름의 클래스 파일을 만들어주세요. 저는 그냥 대충 이름을 지어서 만들어보겠습니다. 여기서 유의해야 할 점은 src/jmh/java 하위에 패키지를 만들지 않고 바로 클래스 파일을 생성하면 안 됩니다. 클래스 파일 하나일 때는 상관이 없는데, 2개 이상이 되는 순간 'Benchmark class should have package other than default.'라는 에러가 발생하니 처음부터 잘 지켜줍시다.
Sample 클래스에서는 아래와 같이 ArrayList와 Stream 간의 성능 비교를 해보겠습니다.
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class Sample {
private List<Integer> list;
@Setup
public void setUp() {
list = new ArrayList<>();
for (int i = 0; i < 5000; i++) {
list.add(i);
}
}
@Benchmark
public void ArrayListBenchmark(Blackhole blackhole) {
blackhole.consume(ArrayListMethod(list));
}
@Benchmark
public void StreamBenchmark(Blackhole blackhole) {
blackhole.consume(StreamMethod(list));
}
private List<Integer> ArrayListMethod(List<Integer> list) {
List <Integer> result = new ArrayList<>();
for(Integer val : list) {
if (val % 5 == 0) {
result.add(val);
}
}
return result;
}
private List<Integer> StreamMethod(List<Integer> list) {
return list.stream().filter(val -> val % 5 == 0).collect(Collectors.toList());
}
}
각 애노테이션에 대한 설명은 생략하겠습니다. 자세한 설명은 여기를 참조해주세요.
8. 터미널을 따로 열어주시고, gradlew가 있는 최상위 디렉토리로 이동해주신 후 아래 명령어를 쳐주세요.
./gradlew jmh
그러면 위 캡처처럼 build가 되고, build 디렉토리도 생성됩니다. 이제 결과를 확인해보겠습니다. 결과는 build/results/jmh 디렉토리에 results.txt라는 이름의 파일로 생성됩니다.
아름다운 모습으로 결과가 저장되어 있습니다. 참고로 위 결과는 나노초(ns) 단위로 결과가 저장된 것이며 애노테이션이나 build.gradle의 jmh 블록에서 설정할 수 있습니다. 그리고 본 글은 빠르게 JMH를 적용해보는 데에 목적을 둔 글이라 벤치마크 정확도는 상당히 떨어집니다. warmup 횟수를 최소화하고 메서드 수행 반복 주기와 횟수도 최소화했기 때문이죠! 참고 바랍니다.
참고 자료
'Dev > Java' 카테고리의 다른 글
[Java] 배열(Array) vs. 배열리스트(ArrayList) vs. 연결리스트(LinkedList) (2) | 2021.03.30 |
---|---|
[Java] 람다식에서 메서드 참조/생성자 참조 사용법 (0) | 2021.03.19 |
[Java] 객체와 클래스, 인스턴스 간 차이 (4) | 2021.03.10 |